@lvce-editor/shared-process 0.95.13 → 0.95.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/shared-process",
3
- "version": "0.95.13",
3
+ "version": "0.95.14",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -18,12 +18,12 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@lvce-editor/assert": "1.7.0",
21
- "@lvce-editor/extension-host-helper-process": "0.95.13",
21
+ "@lvce-editor/extension-host-helper-process": "0.95.14",
22
22
  "@lvce-editor/ipc": "16.6.0",
23
23
  "@lvce-editor/json-rpc": "8.5.0",
24
24
  "@lvce-editor/jsonc-parser": "1.5.0",
25
25
  "@lvce-editor/pretty-error": "2.0.0",
26
- "@lvce-editor/process-explorer": "3.12.1",
26
+ "@lvce-editor/process-explorer": "3.13.0",
27
27
  "@lvce-editor/rpc-registry": "9.43.0",
28
28
  "@lvce-editor/verror": "1.7.0",
29
29
  "is-object": "^1.0.2",
@@ -1,4 +1,5 @@
1
- import { isAbsolute, resolve } from 'node:path'
1
+ import { stat } from 'node:fs/promises'
2
+ import { dirname, isAbsolute, resolve } from 'node:path'
2
3
  import { fileURLToPath, pathToFileURL } from 'node:url'
3
4
  import * as DefaultUrl from '../DefaultUrl/DefaultUrl.js'
4
5
  import * as GetAppWindowOptions from '../GetAppWindowOptions/GetAppWindowOptions.js'
@@ -25,29 +26,40 @@ const getValidatedAppUrl = (url) => {
25
26
  return parsedUrl.toString()
26
27
  }
27
28
 
28
- const getWorkspaceUri = (parsedArgs, workingDirectory) => {
29
+ const getAbsolutePath = (parsedArgs, workingDirectory) => {
29
30
  const path = parsedArgs?._?.at(-1)
30
31
  if (!path || typeof path !== 'string') {
31
32
  return ''
32
33
  }
33
34
  if (path.startsWith('file://')) {
34
- return pathToFileURL(fileURLToPath(path)).toString()
35
+ return fileURLToPath(path)
35
36
  }
36
- const absolutePath = isAbsolute(path) ? path : resolve(workingDirectory, path)
37
- return pathToFileURL(absolutePath).toString()
37
+ return isAbsolute(path) ? path : resolve(workingDirectory, path)
38
38
  }
39
39
 
40
- const getAppWindowUrl = (url, parsedArgs, workingDirectory) => {
40
+ const getAppWindowUrl = async (url, parsedArgs, workingDirectory) => {
41
41
  const parsedUrl = new URL(getValidatedAppUrl(url))
42
- const workspaceUri = getWorkspaceUri(parsedArgs, workingDirectory)
43
- if (workspaceUri) {
44
- parsedUrl.searchParams.set('workspace', workspaceUri)
42
+ const absolutePath = getAbsolutePath(parsedArgs, workingDirectory)
43
+ if (!absolutePath) {
44
+ return parsedUrl.toString()
45
45
  }
46
+ const uri = pathToFileURL(absolutePath).toString()
47
+ try {
48
+ const stats = await stat(absolutePath)
49
+ if (stats.isFile()) {
50
+ parsedUrl.searchParams.set('workspace', pathToFileURL(dirname(absolutePath)).toString())
51
+ parsedUrl.searchParams.set('openUri', uri)
52
+ return parsedUrl.toString()
53
+ }
54
+ } catch {
55
+ // Preserve the existing folder error for paths that do not exist.
56
+ }
57
+ parsedUrl.searchParams.set('workspace', uri)
46
58
  return parsedUrl.toString()
47
59
  }
48
60
 
49
61
  export const createAppWindow = async ({ parsedArgs, preferences, preloadUrl, url = DefaultUrl.defaultUrl, workingDirectory } ) => {
50
- const validatedUrl = getAppWindowUrl(url, parsedArgs, workingDirectory)
62
+ const validatedUrl = await getAppWindowUrl(url, parsedArgs, workingDirectory)
51
63
  const { height, width } = await Screen.getBounds()
52
64
  const windowOptions = await GetAppWindowOptions.getAppWindowOptions({
53
65
  preferences,
@@ -3,6 +3,6 @@ import { fileURLToPath } from 'url'
3
3
 
4
4
  export const getBuiltinExtensionsPath = () => {
5
5
  const staticServerPath = fileURLToPath(import.meta.resolve('@lvce-editor/static-server'))
6
- const builtinExtensionsPath = join(staticServerPath, '..', '..', 'static', 'e8dd865', 'extensions')
6
+ const builtinExtensionsPath = join(staticServerPath, '..', '..', 'static', 'b420a78', 'extensions')
7
7
  return builtinExtensionsPath
8
8
  }
@@ -17,9 +17,13 @@ import * as Logger from '../Logger/Logger.js'
17
17
  // maybe send webview requests directly to preview process
18
18
  export const getElectronFileResponse = async (url, request) => {
19
19
  try {
20
+ const fetchDestination = request?.headers?.['sec-fetch-dest']
21
+ const isRootDocument = url === '/' || url.startsWith('/?')
22
+ const isSecretDocument = isRootDocument && fetchDestination === 'document'
20
23
  const pathName = GetElectronFileResponseRelativePath.getElectronFileResponseRelativePath(url)
21
24
  let absolutePath = GetElectronFileResponseAbsolutePath.getElectronFileResponseAbsolutePath(pathName)
22
25
  let etag
26
+ let preparedContent
23
27
  let stats
24
28
  // TODO when is there no request?
25
29
  if (request) {
@@ -31,19 +35,19 @@ export const getElectronFileResponse = async (url, request) => {
31
35
  // TODO since dynamic data is injected to the stat size is not accurate
32
36
  // which is why this workaround is needed
33
37
  // but it's a bit inefficient
34
- const content = await GetElectronFileResponseContent.getElectronFileResponseContent(request, absolutePath, url)
35
- size = content.byteLength
38
+ preparedContent = await GetElectronFileResponseContent.getElectronFileResponseContent(request, absolutePath, url)
39
+ size = preparedContent.byteLength
36
40
  }
37
- if (request.headers[HttpHeader.IfNotMatch] === etag) {
41
+ if (!isSecretDocument && request.headers[HttpHeader.IfNotMatch] === etag) {
38
42
  const headers = await GetHeaders.getHeaders(absolutePath, pathName, etag, url, size)
39
43
  return GetNotModifiedResponse.getNotModifiedResponse(headers)
40
44
  }
41
45
  }
42
- const content = await GetElectronFileResponseContent.getElectronFileResponseContent(request, absolutePath, url)
46
+ const content = preparedContent || (await GetElectronFileResponseContent.getElectronFileResponseContent(request, absolutePath, url))
43
47
  const size = content.byteLength
44
48
  const headers = await GetHeaders.getHeaders(absolutePath, pathName, etag, url, size)
45
49
 
46
- headers[HttpHeader.CacheControl] = 'public, max-age=0, must-revalidate'
50
+ headers[HttpHeader.CacheControl] = isSecretDocument ? 'no-store' : 'public, max-age=0, must-revalidate'
47
51
  return GetContentResponse.getContentResponse(content, headers)
48
52
  } catch (error) {
49
53
  if (IsEnoentError.isEnoentError(error)) {
@@ -3,6 +3,7 @@ import * as AddCustomPathsToIndexHtml from '../AddCustomPathsToIndexHtml/AddCust
3
3
  import * as Platform from '../Platform/Platform.js'
4
4
  import * as ShouldTranspileTypescript from '../ShouldTranspileTypescript/ShouldTranspileTypescript.js'
5
5
  import * as TranspileTypeScript from '../TranspileTypeScript/TranspileTypeScript.js'
6
+ import * as WebSocketIssuerRegistry from '../WebSocketIssuerRegistry/WebSocketIssuerRegistry.js'
6
7
 
7
8
  const useCache = false // TODO enable this
8
9
 
@@ -23,8 +24,11 @@ export const getElectronFileResponseContent = async (request, absolutePath, url)
23
24
  content = content.toString().replace(' <link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />\n', '')
24
25
  content = await AddCustomPathsToIndexHtml.addCustomPathsToIndexHtml(content)
25
26
  }
26
- if (url === '/') {
27
- content = await AddCustomPathsToIndexHtml.addCustomPathsToIndexHtml(content)
27
+ if (url === '/' || url.startsWith('/?')) {
28
+ const fetchDestination = request?.headers?.['sec-fetch-dest']
29
+ const isDocumentNavigation = fetchDestination === 'document'
30
+ const additionalConfig = isDocumentNavigation ? { webSocketIssuer: WebSocketIssuerRegistry.create() } : {}
31
+ content = await AddCustomPathsToIndexHtml.addCustomPathsToIndexHtml(content, additionalConfig)
28
32
  }
29
33
  if (typeof content === 'string') {
30
34
  content = Buffer.from(content)
@@ -0,0 +1,43 @@
1
+ import * as HttpHeader from '../HttpHeader/HttpHeader.js'
2
+ import * as HttpStatusCode from '../HttpStatusCode/HttpStatusCode.js'
3
+ import * as WebSocketCapability from '../WebSocketCapability/WebSocketCapability.js'
4
+ import * as WebSocketIssuerRegistry from '../WebSocketIssuerRegistry/WebSocketIssuerRegistry.js'
5
+
6
+ const jsonHeaders = {
7
+ [HttpHeader.CacheControl]: 'no-store',
8
+ [HttpHeader.ContentType]: 'application/json; charset=utf-8',
9
+ }
10
+
11
+ const getResponse = (status, value) => {
12
+ return {
13
+ body: Buffer.from(JSON.stringify(value)),
14
+ init: {
15
+ headers: jsonHeaders,
16
+ status,
17
+ },
18
+ }
19
+ }
20
+
21
+ const getBearerToken = (authorization) => {
22
+ if (typeof authorization !== 'string' || !authorization.startsWith('Bearer ')) {
23
+ return ''
24
+ }
25
+ return authorization.slice('Bearer '.length)
26
+ }
27
+
28
+ export const getWebSocketCapabilityResponse = (request) => {
29
+ if (request.method !== 'GET') {
30
+ return getResponse(HttpStatusCode.Forbidden, { error: 'method not allowed' })
31
+ }
32
+ const issuer = getBearerToken(request.headers?.authorization)
33
+ if (!WebSocketIssuerRegistry.isValid(issuer)) {
34
+ return getResponse(HttpStatusCode.Forbidden, { error: 'invalid websocket issuer' })
35
+ }
36
+ const prefix = '/websocket-capabilities/'
37
+ try {
38
+ const target = decodeURIComponent(request.url.slice(prefix.length).split('?')[0])
39
+ return getResponse(HttpStatusCode.Ok, WebSocketCapability.create(target))
40
+ } catch {
41
+ return getResponse(HttpStatusCode.Forbidden, { error: 'websocket target not allowed' })
42
+ }
43
+ }
@@ -4,4 +4,6 @@ export const name = 'HandleMessagePortForExtensionHostHelperProcess'
4
4
 
5
5
  export const Commands = {
6
6
  handleMessagePortForExtensionHostHelperProcess: HandleMessagePortForExtensionHostHelperProcess.handleMessagePortForExtensionHostHelperProcess,
7
+ handlePreloadedMessagePortForExtensionHostHelperProcess:
8
+ HandleMessagePortForExtensionHostHelperProcess.handlePreloadedMessagePortForExtensionHostHelperProcess,
7
9
  }
@@ -23,3 +23,22 @@ export const handleMessagePortForExtensionHostHelperProcess = async (rendererWor
23
23
  // }
24
24
  // rendererWorkerIpc.on('close', cleanup)
25
25
  }
26
+
27
+ export const handlePreloadedMessagePortForExtensionHostHelperProcess = async (
28
+ port,
29
+ modulePath,
30
+ ) => {
31
+ Assert.object(port)
32
+ Assert.string(modulePath)
33
+ const ipc = await ExtensionHostHelperProcessIpc.create({
34
+ method: IpcParentType.ElectronUtilityProcess,
35
+ })
36
+ HandleIpc.handleIpc(ipc)
37
+ try {
38
+ await JsonRpc.invoke(ipc, 'LoadFile.loadFile', modulePath)
39
+ await JsonRpc.invokeAndTransfer(ipc, 'HandleElectronMessagePort.handleElectronMessagePort', port, IpcId.ExtensionHostWorker)
40
+ } catch (error) {
41
+ ipc.dispose()
42
+ throw error
43
+ }
44
+ }
@@ -1,6 +1,7 @@
1
1
  import { join } from 'path'
2
2
  import * as Assert from '../Assert/Assert.js'
3
3
  import * as GetElectronFileResponse from '../GetElectronFileResponse/GetElectronFileResponse.js'
4
+ import * as GetWebSocketCapabilityResponse from '../GetWebSocketCapabilityResponse/GetWebSocketCapabilityResponse.js'
4
5
  import * as HandleRequestTest from '../HandleRequestTest/HandleRequestTest.js'
5
6
  import * as HttpServerResponse from '../HttpServerResponse/HttpServerResponse.js'
6
7
  import * as StaticPath from '../StaticPath/StaticPath.js'
@@ -17,6 +18,10 @@ export const handleRequest = async (socket, request) => {
17
18
  const indexHtmlPath = join(staticPath, 'index.html')
18
19
  return HandleRequestTest.handleRequestTest(socket, request, indexHtmlPath)
19
20
  }
21
+ if (request.url.startsWith('/websocket-capabilities/')) {
22
+ const response = GetWebSocketCapabilityResponse.getWebSocketCapabilityResponse(request)
23
+ return HttpServerResponse.send(request, socket, response)
24
+ }
20
25
  const fileResponse = await GetElectronFileResponse.getElectronFileResponse(request.url, request)
21
26
  HttpServerResponse.send(request, socket, fileResponse)
22
27
  }
@@ -5,6 +5,29 @@ import * as HandleWebSocketModule from '../HandleWebSocketModule/HandleWebSocket
5
5
  import * as IsAllowedWebSocketOrigin from '../IsAllowedWebSocketOrigin/IsAllowedWebSocketOrigin.js'
6
6
  import * as RejectWebSocket from '../RejectWebSocket/RejectWebSocket.js'
7
7
  import { VError } from '../VError/VError.js'
8
+ import * as WebSocketCapabilityRegistry from '../WebSocketCapabilityRegistry/WebSocketCapabilityRegistry.js'
9
+
10
+ const capabilityPath = '/websocket/capability'
11
+
12
+ const getCapabilityToken = (message) => {
13
+ const header = message.headers?.['sec-websocket-protocol']
14
+ if (typeof header !== 'string') {
15
+ return ''
16
+ }
17
+ const protocols = header.split(',').map((protocol) => protocol.trim())
18
+ if (!protocols.includes(WebSocketCapabilityRegistry.capabilityProtocol)) {
19
+ return ''
20
+ }
21
+ const capabilityProtocol = protocols.find((protocol) => protocol.startsWith(WebSocketCapabilityRegistry.capabilityProtocolPrefix))
22
+ return capabilityProtocol?.slice(WebSocketCapabilityRegistry.capabilityProtocolPrefix.length) || ''
23
+ }
24
+
25
+ const sanitizeProtocols = (message) => {
26
+ message.headers = {
27
+ ...message.headers,
28
+ 'sec-websocket-protocol': WebSocketCapabilityRegistry.capabilityProtocol,
29
+ }
30
+ }
8
31
 
9
32
  export const handleWebSocket = async (handle, message) => {
10
33
  try {
@@ -15,10 +38,21 @@ export const handleWebSocket = async (handle, message) => {
15
38
  return
16
39
  }
17
40
  const { url } = message
18
- const type = GetTypeFromUrl.getTypeFromUrl(url)
41
+ if (url.split('?')[0] !== capabilityPath) {
42
+ RejectWebSocket.rejectWebSocket(handle)
43
+ return
44
+ }
45
+ const token = getCapabilityToken(message)
46
+ const capability = WebSocketCapabilityRegistry.consume(token)
47
+ if (!capability) {
48
+ RejectWebSocket.rejectWebSocket(handle)
49
+ return
50
+ }
51
+ sanitizeProtocols(message)
52
+ const type = capability.target
19
53
  handle.pause()
20
54
  const module = HandleWebSocketModule.load(type)
21
- module.handleWebSocket(message, handle, type)
55
+ await module.handleWebSocket(message, handle, type, capability)
22
56
  } catch (error) {
23
57
  DestroyWebSocket.destroySocket(handle)
24
58
  throw new VError(error, `Failed to connect to websocket`)
@@ -1,6 +1,22 @@
1
- import * as HandleIncomingIpc from '../HandleIncomingIpc/HandleIncomingIpc.js'
2
- import * as IpcId from '../IpcId/IpcId.js'
1
+
2
+ import * as ExtensionHostHelperProcessIpc from '../ExtensionHostHelperProcessIpc/ExtensionHostHelperProcessIpc.js'
3
+ import * as HandleIpc from '../HandleIpc/HandleIpc.js'
4
+ import * as IpcParentType from '../IpcParentType/IpcParentType.js'
5
+ import * as JsonRpc from '../JsonRpc/JsonRpc.js'
3
6
 
4
- export const handleWebSocket = (message, handle) => {
5
- return HandleIncomingIpc.handleIncomingIpc(IpcId.ExtensionHostHelperProcess, handle, message)
7
+ export const handleWebSocket = async (message, handle, type, capability) => {
8
+ if (!capability.modulePath) {
9
+ throw new Error('extension helper capability is missing a module path')
10
+ }
11
+ const ipc = await ExtensionHostHelperProcessIpc.create({
12
+ method: IpcParentType.NodeForkedProcess,
13
+ })
14
+ HandleIpc.handleIpc(ipc)
15
+ try {
16
+ await JsonRpc.invoke(ipc, 'LoadFile.loadFile', capability.modulePath)
17
+ await JsonRpc.invokeAndTransfer(ipc, 'HandleWebSocket.handleWebSocket', handle, message)
18
+ } catch (error) {
19
+ ipc.dispose()
20
+ throw error
21
+ }
6
22
  }
@@ -154,6 +154,8 @@ export const load = (moduleId) => {
154
154
  return import('../Terminal/Terminal.ipc.js')
155
155
  case ModuleId.TextDocument:
156
156
  return import('../TextDocument/TextDocument.ipc.js')
157
+ case ModuleId.WebSocketCapability:
158
+ return import('../WebSocketCapability/WebSocketCapability.ipc.js')
157
159
  case ModuleId.WebViewServer:
158
160
  return import('../WebViewServer/WebViewServer.ipc.js')
159
161
  case ModuleId.Window:
@@ -82,3 +82,4 @@ export const SendMessagePortToMainProcess = 88
82
82
  export const SecretStorage = 89
83
83
  export const ChromeCookieImport = 90
84
84
  export const FirefoxCookieImport = 91
85
+ export const WebSocketCapability = 92
@@ -184,6 +184,7 @@ export const getModuleId = (commandId) => {
184
184
  case 'HandleMessagePortForEmbedsProcess.handleMessagePortForEmbedsProcess':
185
185
  return ModuleId.HandleMessagePortForEmbedsProcess
186
186
  case 'HandleMessagePortForExtensionHostHelperProcess.handleMessagePortForExtensionHostHelperProcess':
187
+ case 'HandleMessagePortForExtensionHostHelperProcess.handlePreloadedMessagePortForExtensionHostHelperProcess':
187
188
  return ModuleId.HandleMessagePortForExtensionHostHelperProcess
188
189
  case 'HandleMessagePortForFileSystemProcess.handleMessagePortForFileSystemProcess':
189
190
  return ModuleId.HandleMessagePortForFileSystemProcess
@@ -300,6 +301,10 @@ export const getModuleId = (commandId) => {
300
301
  return ModuleId.Terminal
301
302
  case 'Transferrable.transfer':
302
303
  return ModuleId.Transferrable
304
+ case 'WebSocketCapability.create':
305
+ case 'WebSocketCapability.createExtensionNodeRpc':
306
+ case 'WebSocketCapability.createLegacyExtensionNodeRpc':
307
+ return ModuleId.WebSocketCapability
303
308
  case 'WebViewServer.create':
304
309
  case 'WebViewServer.registerProtocol':
305
310
  case 'WebViewServer.setHandler':
@@ -61,11 +61,11 @@ export const getSetupName = () => {
61
61
  return 'Lvce-Setup'
62
62
  }
63
63
 
64
- export const version = '0.95.13'
64
+ export const version = '0.95.14'
65
65
 
66
- export const commit = 'e8dd865'
66
+ export const commit = 'b420a78'
67
67
 
68
- export const date = '2026-08-01T08:43:45.000Z'
68
+ export const date = '2026-08-01T16:57:35.000Z'
69
69
 
70
70
  export const getVersion = () => {
71
71
  return version
@@ -2,5 +2,5 @@ import { join } from 'node:path'
2
2
  import * as Root from '../Root/Root.js'
3
3
 
4
4
  export const getPreloadUrl = () => {
5
- return join(Root.root, 'static', 'e8dd865', 'packages', 'preload', 'dist', 'index.js')
5
+ return join(Root.root, 'static', 'b420a78', 'packages', 'preload', 'dist', 'index.js')
6
6
  }
@@ -0,0 +1,9 @@
1
+ import * as WebSocketCapability from './WebSocketCapability.js'
2
+
3
+ export const name = 'WebSocketCapability'
4
+
5
+ export const commandMap = {
6
+ create: WebSocketCapability.create,
7
+ createExtensionNodeRpc: WebSocketCapability.createExtensionNodeRpc,
8
+ createLegacyExtensionNodeRpc: WebSocketCapability.createLegacyExtensionNodeRpc,
9
+ }
@@ -0,0 +1,53 @@
1
+ import * as WebSocketCapabilityRegistry from '../WebSocketCapabilityRegistry/WebSocketCapabilityRegistry.js'
2
+
3
+ const allowedTargets = new Set([
4
+ 'clipboard-process',
5
+ 'file-system-process',
6
+ 'process-explorer',
7
+ 'search-process',
8
+ 'shared-process',
9
+ 'terminal-process',
10
+ ])
11
+
12
+
13
+
14
+
15
+
16
+
17
+ const createConnectionInfo = (token) => {
18
+ return {
19
+ protocols: WebSocketCapabilityRegistry.getProtocols(token),
20
+ url: '/websocket/capability',
21
+ }
22
+ }
23
+
24
+ export const create = (target) => {
25
+ if (!allowedTargets.has(target)) {
26
+ throw new Error(`WebSocket target ${target} is not allowed`)
27
+ }
28
+ return createConnectionInfo(WebSocketCapabilityRegistry.create(target))
29
+ }
30
+
31
+ export const createExtensionNodeRpc = (extensionId, rpcId, modulePath) => {
32
+ if (!extensionId || !rpcId || !modulePath) {
33
+ throw new TypeError('extension node rpc capability requires extensionId, rpcId and modulePath')
34
+ }
35
+ return createConnectionInfo(
36
+ WebSocketCapabilityRegistry.create('extension-host-helper-process', {
37
+ extensionId,
38
+ modulePath,
39
+ rpcId,
40
+ }),
41
+ )
42
+ }
43
+
44
+ export const createLegacyExtensionNodeRpc = (modulePath) => {
45
+ if (!modulePath) {
46
+ throw new TypeError('legacy extension node rpc capability requires a modulePath')
47
+ }
48
+ return createConnectionInfo(
49
+ WebSocketCapabilityRegistry.create('extension-host-helper-process', {
50
+ modulePath,
51
+ }),
52
+ )
53
+ }
@@ -0,0 +1,57 @@
1
+ import { randomBytes } from 'node:crypto'
2
+
3
+ export const capabilityProtocol = 'lvce-rpc'
4
+ export const capabilityProtocolPrefix = 'lvce-capability.'
5
+ export const capabilityTtl = 60_000
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+ const capabilities = new Map ()
16
+
17
+ const cleanup = (now) => {
18
+ for (const [token, capability] of capabilities) {
19
+ if (capability.expiresAt <= now) {
20
+ capabilities.delete(token)
21
+ }
22
+ }
23
+ }
24
+
25
+ export const create = (target, options = {}) => {
26
+ const now = Date.now()
27
+ cleanup(now)
28
+ const token = randomBytes(32).toString('base64url')
29
+ capabilities.set(token, {
30
+ ...options,
31
+ expiresAt: now + capabilityTtl,
32
+ target,
33
+ })
34
+ return token
35
+ }
36
+
37
+ export const consume = (token) => {
38
+ const now = Date.now()
39
+ cleanup(now)
40
+ const capability = capabilities.get(token)
41
+ if (!capability) {
42
+ return undefined
43
+ }
44
+ capabilities.delete(token)
45
+ if (capability.expiresAt <= now) {
46
+ return undefined
47
+ }
48
+ return capability
49
+ }
50
+
51
+ export const getProtocols = (token) => {
52
+ return [capabilityProtocol, `${capabilityProtocolPrefix}${token}`]
53
+ }
54
+
55
+ export const clear = () => {
56
+ capabilities.clear()
57
+ }
@@ -0,0 +1,32 @@
1
+ import { randomBytes } from 'node:crypto'
2
+
3
+ export const issuerTtl = 24 * 60 * 60 * 1000
4
+
5
+ const issuers = new Map ()
6
+
7
+ const cleanup = (now) => {
8
+ for (const [issuer, expiresAt] of issuers) {
9
+ if (expiresAt <= now) {
10
+ issuers.delete(issuer)
11
+ }
12
+ }
13
+ }
14
+
15
+ export const create = () => {
16
+ const now = Date.now()
17
+ cleanup(now)
18
+ const issuer = randomBytes(32).toString('base64url')
19
+ issuers.set(issuer, now + issuerTtl)
20
+ return issuer
21
+ }
22
+
23
+ export const isValid = (issuer) => {
24
+ const now = Date.now()
25
+ cleanup(now)
26
+ const expiresAt = issuers.get(issuer)
27
+ return typeof expiresAt === 'number' && expiresAt > now
28
+ }
29
+
30
+ export const clear = () => {
31
+ issuers.clear()
32
+ }