1import assert from 'node:assert/strict'2import test from 'node:test'3import {4 aliasIdFromPath,5 editAliasUrl,6 generateWriteCapability,7 PersistenceClient,8 PersistenceError,9 readAliasUrl,10 stableRenderUrl,11 writeCapabilityFromHash,12} from '../src/persistence.js'1314const aliasId = 'AbCdEfGhIjKlMnOp'15const writeCapability = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'16const renderer = {17 unit: 'vegalite',18 build: 'vegalite-6.4.3-vega-6.3.1-unit-1',19 pipeline: ['vegalite', 'vega'],20}21const state = {22 type: 'd2',23 source: 'a -> b',24 options: {},25 meta: { title: 'A to B', description: '' },26 presentation: { background: '', padding: 0, frame: false },27}28const encryptedBlob = {29 v: 1,30 alg: 'A256GCM',31 iv: 'AAAAAAAAAAAAAAAA',32 ciphertext: 'AAAAAAAAAAAAAAAAAAAAAA',33}34const lockedPayload = {35 mode: 'locked',36 encryptedContent: encryptedBlob,37 encryptedMetadata: { ...encryptedBlob, iv: 'BBBBBBBBBBBBBBBB' },38 keyEnvelope: {39 v: 1,40 kdf: {41 name: 'PBKDF2',42 hash: 'SHA-256',43 iterations: 600_000,44 salt: 'AAAAAAAAAAAAAAAAAAAAAA',45 },46 wrap: { ...encryptedBlob, iv: 'CCCCCCCCCCCCCCCC' },47 },48}4950function response(overrides = {}) {51 return {52 aliasId,53 contentId: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',54 renderId: 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB',55 revision: 1,56 mode: 'open',57 metadata: state.meta,58 diagram: {59 type: state.type,60 source: state.source,61 options: state.options,62 presentation: state.presentation,63 },64 createdAt: 1,65 updatedAt: 1,66 ...overrides,67 }68}6970test('builds short read and capability-bearing edit links', () => {71 assert.equal(aliasIdFromPath(`/d/${aliasId}`), aliasId)72 assert.equal(aliasIdFromPath('/'), null)73 assert.equal(readAliasUrl('https://diagram.zip', aliasId), `https://diagram.zip/d/${aliasId}`)74 assert.equal(editAliasUrl('https://diagram.zip', aliasId, writeCapability), `https://diagram.zip/d/${aliasId}#w=${writeCapability}`)75 assert.equal(stableRenderUrl('https://diagram.zip', aliasId), `https://diagram.zip/api/v1/aliases/${aliasId}/renders/svg`)76 assert.equal(stableRenderUrl('https://diagram.zip', aliasId, 'svg', 'auto-framed'), `https://diagram.zip/api/v1/aliases/${aliasId}/renders/svg?appearance=auto-framed`)77 assert.throws(() => stableRenderUrl('https://diagram.zip', aliasId, 'png', 'dark-framed'), /require SVG/)78 assert.equal(writeCapabilityFromHash(`#w=${writeCapability}`), writeCapability)79})8081test('generates a 256-bit Base64URL write capability', () => {82 const first = generateWriteCapability()83 const second = generateWriteCapability()84 assert.match(first, /^[A-Za-z0-9_-]{43}$/)85 assert.notEqual(first, second)86})8788test('creates an alias without putting the capability in the body', async () => {89 let captured90 const client = new PersistenceClient({91 origin: 'https://diagram.zip',92 fetchImpl: async (url, init) => {93 captured = { url: url.toString(), init }94 return Response.json(response(), { status: 201 })95 },96 })97 const result = await client.createAlias(state)98 assert.equal(captured.url, 'https://diagram.zip/api/v1/aliases')99 assert.match(captured.init.headers.Authorization, /^Bearer [A-Za-z0-9_-]{43}$/)100 assert.equal(captured.init.body.includes(captured.init.headers.Authorization.slice(7)), false)101 assert.equal(result.state.source, state.source)102 assert.match(result.writeCapability, /^[A-Za-z0-9_-]{43}$/)103})104105test('updates an alias with a quoted revision and capability', async () => {106 let captured107 const client = new PersistenceClient({108 fetchImpl: async (url, init) => {109 captured = { url: url.toString(), init }110 return Response.json(response({ revision: 2 }))111 },112 })113 const result = await client.updateAlias(aliasId, state, 1, writeCapability)114 assert.equal(captured.url, `http://localhost/api/v1/aliases/${aliasId}`)115 assert.equal(captured.init.headers.Authorization, `Bearer ${writeCapability}`)116 assert.equal(captured.init.headers['If-Match'], '"1"')117 assert.equal(result.revision, 2)118})119120test('creates a locked alias without plaintext diagram fields', async () => {121 let captured122 const client = new PersistenceClient({123 fetchImpl: async (url, init) => {124 captured = { url: url.toString(), init }125 return Response.json(response({126 ...lockedPayload,127 diagram: undefined,128 metadata: undefined,129 }), { status: 201 })130 },131 })132 const result = await client.createLockedAlias(lockedPayload)133 const body = JSON.parse(captured.init.body)134 assert.equal(body.mode, 'locked')135 assert.equal('diagram' in body, false)136 assert.equal('metadata' in body, false)137 assert.deepEqual(result.encryptedContent, lockedPayload.encryptedContent)138 assert.match(result.writeCapability, /^[A-Za-z0-9_-]{43}$/)139})140141test('updates a locked alias with revision and capability headers', async () => {142 let captured143 const client = new PersistenceClient({144 fetchImpl: async (url, init) => {145 captured = { url: url.toString(), init }146 return Response.json(response({147 ...lockedPayload,148 diagram: undefined,149 metadata: undefined,150 revision: 2,151 }))152 },153 })154 const result = await client.updateLockedAlias(aliasId, lockedPayload, 1, writeCapability)155 assert.equal(captured.init.headers.Authorization, `Bearer ${writeCapability}`)156 assert.equal(captured.init.headers['If-Match'], '"1"')157 assert.equal(result.mode, 'locked')158 assert.equal(result.revision, 2)159})160161test('uploads an open content-addressed render', async () => {162 let captured163 const client = new PersistenceClient({164 fetchImpl: async (url, init) => {165 captured = { url: url.toString(), init }166 return new Response(null, { status: 204 })167 },168 })169 const svg = new Blob(['<svg/>'], { type: 'image/svg+xml' })170 await client.uploadRender({171 aliasId,172 renderId: writeCapability,173 revision: 2,174 writeCapability,175 format: 'svg',176 mode: 'open',177 render: svg,178 renderer,179 })180 assert.equal(captured.url, `http://localhost/api/v1/aliases/${aliasId}/renders/svg`)181 assert.equal(captured.init.headers['If-Match'], '"2"')182 assert.equal(captured.init.headers['X-Render-Id'], writeCapability)183 assert.equal(captured.init.headers['X-Renderer-Unit'], 'vegalite')184 assert.equal(captured.init.headers['X-Renderer-Build'], 'vegalite-6.4.3-vega-6.3.1-unit-1')185 assert.equal(captured.init.headers['X-Renderer-Pipeline'], 'vegalite,vega')186 assert.equal(captured.init.headers['Content-Type'], 'image/svg+xml')187 assert.equal(captured.init.body, svg)188})189190test('uploads and retrieves an opaque locked render', async () => {191 const requests = []192 const client = new PersistenceClient({193 fetchImpl: async (url, init) => {194 requests.push({ url: url.toString(), init })195 if (init?.method === 'PUT') return new Response(null, { status: 204 })196 return Response.json(encryptedBlob)197 },198 })199 await client.uploadRender({200 aliasId,201 renderId: writeCapability,202 revision: 1,203 writeCapability,204 format: 'png',205 mode: 'locked',206 render: encryptedBlob,207 renderer,208 })209 assert.equal(requests[0].init.headers['Content-Type'], 'application/json')210 assert.deepEqual(JSON.parse(requests[0].init.body), encryptedBlob)211 assert.deepEqual(await client.getEncryptedRender(aliasId, 'png'), encryptedBlob)212 assert.equal(requests[1].url, `http://localhost/api/v1/aliases/${aliasId}/renders/png?encrypted=1`)213})214215test('reads the published renderer identity without downloading render bytes', async () => {216 let captured217 const client = new PersistenceClient({218 fetchImpl: async (url, init) => {219 captured = { url: url.toString(), init }220 return new Response(null, {221 headers: {222 'X-Renderer-Unit': renderer.unit,223 'X-Renderer-Build': renderer.build,224 'X-Renderer-Pipeline': renderer.pipeline.join(','),225 },226 })227 },228 })229 assert.deepEqual(await client.getRenderIdentity(aliasId, 'svg', 'locked'), renderer)230 assert.equal(captured.url, `http://localhost/api/v1/aliases/${aliasId}/renders/svg?encrypted=1`)231 assert.equal(captured.init.method, 'HEAD')232})233234test('treats a missing published render as stale', async () => {235 const client = new PersistenceClient({236 fetchImpl: async () => new Response(null, { status: 404 }),237 })238 assert.equal(await client.getRenderIdentity(aliasId), null)239})240241test('surfaces structured persistence errors', async () => {242 const client = new PersistenceClient({243 fetchImpl: async () => Response.json({244 error: { code: 'revision_conflict', message: 'The alias changed.' },245 }, { status: 412 }),246 })247 await assert.rejects(248 client.updateAlias(aliasId, state, 1, writeCapability),249 error => error instanceof PersistenceError && error.status === 412 && error.code === 'revision_conflict',250 )251})