|
| 1 | +import { test } from "node:test"; |
| 2 | +import assert from "node:assert/strict"; |
| 3 | +import { mkdtempSync } from "node:fs"; |
| 4 | +import { tmpdir } from "node:os"; |
| 5 | +import { join } from "node:path"; |
| 6 | +import { createApp } from "../src/api/app.ts"; |
| 7 | +import { createDeployStore } from "../src/deploy/deploy-store.ts"; |
| 8 | +import { createDeployService } from "../src/deploy/deploy-service.ts"; |
| 9 | +import { createAclStore } from "../src/acl/acl-store.ts"; |
| 10 | +import { createDirectoryStore } from "../src/directory/directory-store.ts"; |
| 11 | +import { createIdentityService } from "../src/identity/identity-service.ts"; |
| 12 | +import { createMemorySessionStore } from "../src/sessions/memory-session-store.ts"; |
| 13 | +import { scopeId } from "../src/types.ts"; |
| 14 | + |
| 15 | +async function setup() { |
| 16 | + const acl = createAclStore(); |
| 17 | + const deploy = createDeployService({ |
| 18 | + deployStore: createDeployStore(), |
| 19 | + provider: { |
| 20 | + profile: { managedScaleToZero: false }, |
| 21 | + apply: async () => ({ |
| 22 | + host: "127.0.0.1", |
| 23 | + port: 19999, |
| 24 | + publicUrl: "https://research-artifact.apps.example/", |
| 25 | + }), |
| 26 | + destroy: async () => {}, |
| 27 | + }, |
| 28 | + auditLog: { record() {}, events: async () => [], tail: async () => [] }, |
| 29 | + acl, |
| 30 | + deployDir: mkdtempSync(join(tmpdir(), "deployment-link-share-")), |
| 31 | + }); |
| 32 | + const directory = createDirectoryStore(); |
| 33 | + await directory.replaceChannels( |
| 34 | + [{ channelId: "CRESEARCH", name: "research", isPrivate: true }], |
| 35 | + [ |
| 36 | + { channelId: "CRESEARCH", principalId: "U1" }, |
| 37 | + { channelId: "CRESEARCH", principalId: "U2" }, |
| 38 | + ], |
| 39 | + ); |
| 40 | + const app = createApp({ |
| 41 | + deploy, |
| 42 | + acl, |
| 43 | + directory, |
| 44 | + sessions: createMemorySessionStore(), |
| 45 | + identity: createIdentityService(), |
| 46 | + publicWebUrl: "https://qm.example", |
| 47 | + } as unknown as Parameters<typeof createApp>[0]); |
| 48 | + const deployment = await app.deploy({ |
| 49 | + ownerScopeId: scopeId("personal", "U1"), |
| 50 | + createdBy: "U1", |
| 51 | + entrypoint: "node server.js", |
| 52 | + files: [], |
| 53 | + name: "research-artifact", |
| 54 | + }); |
| 55 | + return { app, deployment }; |
| 56 | +} |
| 57 | + |
| 58 | +test("posting an owned deployment URL shares it read-only with the conversation scope", async () => { |
| 59 | + const { app, deployment } = await setup(); |
| 60 | + assert.equal((await app.reachDeployment(deployment.id, "U2")).status, "denied"); |
| 61 | + |
| 62 | + await app.ingestSurfaceEvents([ |
| 63 | + { |
| 64 | + container: "CRESEARCH", |
| 65 | + ts: "1.0", |
| 66 | + authorId: "U1", |
| 67 | + text: "Here it is: https://research-artifact.apps.example/", |
| 68 | + kind: "channel", |
| 69 | + }, |
| 70 | + ]); |
| 71 | + |
| 72 | + assert.equal((await app.reachDeployment(deployment.id, "U2")).status, "ok"); |
| 73 | + assert.deepEqual(await app.deploymentGrantees(deployment.id), [ |
| 74 | + { scope: scopeId("channel", "CRESEARCH"), permission: "read" }, |
| 75 | + ]); |
| 76 | +}); |
| 77 | + |
| 78 | +test("trusted /d links share, while lookalike links and non-owner posts do not", async () => { |
| 79 | + const first = await setup(); |
| 80 | + await first.app.ingestSurfaceEvents([ |
| 81 | + { |
| 82 | + container: "CRESEARCH", |
| 83 | + ts: "1.0", |
| 84 | + authorId: "U1", |
| 85 | + text: "<https://qm.example/d/research-artifact/|open dashboard>", |
| 86 | + kind: "channel", |
| 87 | + }, |
| 88 | + ]); |
| 89 | + assert.equal((await first.app.reachDeployment(first.deployment.id, "U2")).status, "ok"); |
| 90 | + |
| 91 | + for (const [authorId, text] of [ |
| 92 | + ["U1", "https://attacker.example/d/research-artifact/"], |
| 93 | + ["U2", "https://research-artifact.apps.example/"], |
| 94 | + ]) { |
| 95 | + const { app, deployment } = await setup(); |
| 96 | + await app.ingestSurfaceEvents([{ container: "CRESEARCH", ts: "2.0", authorId, text, kind: "channel" }]); |
| 97 | + assert.equal((await app.reachDeployment(deployment.id, "U2")).status, "denied"); |
| 98 | + } |
| 99 | +}); |
0 commit comments