@astrale-os/sdk 0.4.13 → 0.4.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/dist/domain/extend-functions.d.ts.map +1 -1
- package/dist/domain/extend-functions.js +2 -1
- package/dist/domain/extend-functions.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/linter/project.d.ts +6 -0
- package/dist/linter/project.d.ts.map +1 -1
- package/dist/linter/project.js +69 -7
- package/dist/linter/project.js.map +1 -1
- package/dist/server/auxiliary-routes.d.ts +5 -1
- package/dist/server/auxiliary-routes.d.ts.map +1 -1
- package/dist/server/auxiliary-routes.js +15 -6
- package/dist/server/auxiliary-routes.js.map +1 -1
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/service-entry.d.ts +32 -0
- package/dist/server/service-entry.d.ts.map +1 -0
- package/dist/server/service-entry.js +150 -0
- package/dist/server/service-entry.js.map +1 -0
- package/dist/server/worker-entry.d.ts +7 -3
- package/dist/server/worker-entry.d.ts.map +1 -1
- package/dist/server/worker-entry.js +7 -1
- package/dist/server/worker-entry.js.map +1 -1
- package/dist/service/functions.d.ts +148 -0
- package/dist/service/functions.d.ts.map +1 -0
- package/dist/service/functions.js +115 -0
- package/dist/service/functions.js.map +1 -0
- package/dist/service/index.d.ts +3 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +2 -0
- package/dist/service/index.js.map +1 -0
- package/package.json +1 -1
- package/src/domain/extend-functions.ts +5 -1
- package/src/index.ts +18 -0
- package/src/linter/docs/RULES.md +1 -1
- package/src/linter/project.ts +88 -6
- package/src/server/auxiliary-routes.ts +34 -7
- package/src/server/index.ts +10 -2
- package/src/server/service-entry.ts +209 -0
- package/src/server/worker-entry.ts +18 -5
- package/src/service/functions.ts +164 -0
- package/src/service/index.ts +18 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
|
|
2
|
+
import type { FunctionSchema } from '@astrale-os/kernel-core/domain'
|
|
3
|
+
|
|
4
|
+
import { deriveAllowedAlgorithms } from '@astrale-os/kernel-core'
|
|
5
|
+
import { functionBindingSchema, functionOutputSchema } from '@astrale-os/kernel-core/domain'
|
|
6
|
+
import { importJWK, jwtVerify } from 'jose'
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
|
|
9
|
+
import type { RemoteIdentityConfig } from '../auth/identity.js'
|
|
10
|
+
|
|
11
|
+
import { signCredential } from '../auth/sign.js'
|
|
12
|
+
|
|
13
|
+
export const SERVICE_FUNCTIONS_PATH = '/_astrale/functions'
|
|
14
|
+
export const SERVICE_FUNCTIONS_PURPOSE = 'astrale.service-functions.v1'
|
|
15
|
+
|
|
16
|
+
const JsonSchema = z.record(z.string(), z.unknown())
|
|
17
|
+
|
|
18
|
+
export const ServiceFunctionManifestEntrySchema = z.object({
|
|
19
|
+
slug: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
20
|
+
ref: z.string().regex(/^function\.[a-z][a-z0-9-]*$/),
|
|
21
|
+
inputSchema: JsonSchema,
|
|
22
|
+
outputSchema: JsonSchema,
|
|
23
|
+
output: functionOutputSchema.default('value'),
|
|
24
|
+
binding: functionBindingSchema.unwrap(),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const ServiceFunctionManifestSchema = z.object({
|
|
28
|
+
version: z.literal(1),
|
|
29
|
+
service: z.object({ issuer: z.string().min(1), subject: z.string().min(1) }),
|
|
30
|
+
functions: z.array(ServiceFunctionManifestEntrySchema).max(500),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const ServiceFunctionDiscoveryRequestSchema = z.object({
|
|
34
|
+
kernelIssuer: z.string().url(),
|
|
35
|
+
nonce: z.string().min(16).max(256),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export const ServiceFunctionDiscoveryResponseSchema = ServiceFunctionManifestSchema.extend({
|
|
39
|
+
identity: z.object({ credential: z.string().min(1) }),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export type ServiceFunctionManifestEntry = z.infer<typeof ServiceFunctionManifestEntrySchema>
|
|
43
|
+
export type ServiceFunctionManifest = z.infer<typeof ServiceFunctionManifestSchema>
|
|
44
|
+
export type ServiceFunctionDiscoveryRequest = z.infer<typeof ServiceFunctionDiscoveryRequestSchema>
|
|
45
|
+
export type ServiceFunctionDiscoveryResponse = z.infer<
|
|
46
|
+
typeof ServiceFunctionDiscoveryResponseSchema
|
|
47
|
+
>
|
|
48
|
+
|
|
49
|
+
export function toServiceFunctionManifestEntries(
|
|
50
|
+
schemas: readonly FunctionSchema[],
|
|
51
|
+
): ServiceFunctionManifestEntry[] {
|
|
52
|
+
return schemas
|
|
53
|
+
.map((schema) => {
|
|
54
|
+
const slug = schema.ref.slice('function.'.length)
|
|
55
|
+
return ServiceFunctionManifestEntrySchema.parse({
|
|
56
|
+
slug,
|
|
57
|
+
ref: schema.ref,
|
|
58
|
+
inputSchema: schema.inputSchema,
|
|
59
|
+
outputSchema: schema.outputSchema,
|
|
60
|
+
output: schema.output ?? 'value',
|
|
61
|
+
binding: schema.binding,
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
.sort((a, b) => a.slug.localeCompare(b.slug))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function signServiceFunctionManifest(
|
|
68
|
+
manifest: ServiceFunctionManifest,
|
|
69
|
+
request: ServiceFunctionDiscoveryRequest,
|
|
70
|
+
identity: RemoteIdentityConfig,
|
|
71
|
+
): Promise<ServiceFunctionDiscoveryResponse> {
|
|
72
|
+
const parsedManifest = ServiceFunctionManifestSchema.parse(manifest)
|
|
73
|
+
const manifestHash = await hashServiceFunctionManifest(parsedManifest)
|
|
74
|
+
const credential = await signCredential(
|
|
75
|
+
{ purpose: SERVICE_FUNCTIONS_PURPOSE, nonce: request.nonce, manifestHash },
|
|
76
|
+
{
|
|
77
|
+
issuer: identity.issuer,
|
|
78
|
+
subject: identity.subject,
|
|
79
|
+
audience: request.kernelIssuer,
|
|
80
|
+
privateKey: identity.privateKey,
|
|
81
|
+
ttl: '2m',
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
return { ...parsedManifest, identity: { credential } }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function verifyServiceFunctionManifest(
|
|
88
|
+
input: unknown,
|
|
89
|
+
expected: {
|
|
90
|
+
kernelIssuer: string
|
|
91
|
+
nonce: string
|
|
92
|
+
serviceIssuer: string
|
|
93
|
+
serviceSubject: string
|
|
94
|
+
publicKey: JsonWebKey
|
|
95
|
+
},
|
|
96
|
+
): Promise<ServiceFunctionDiscoveryResponse> {
|
|
97
|
+
const response = ServiceFunctionDiscoveryResponseSchema.parse(input)
|
|
98
|
+
if (
|
|
99
|
+
response.service.issuer !== expected.serviceIssuer ||
|
|
100
|
+
response.service.subject !== expected.serviceSubject
|
|
101
|
+
) {
|
|
102
|
+
throw new Error('service function manifest identity does not match the deployed Service')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const algorithms = deriveAllowedAlgorithms(expected.publicKey)
|
|
106
|
+
const algorithm = algorithms[0]
|
|
107
|
+
if (!algorithm) throw new Error('service function manifest public key has no supported algorithm')
|
|
108
|
+
const key = await importJWK(expected.publicKey, algorithm)
|
|
109
|
+
const verified = await jwtVerify(response.identity.credential, key, {
|
|
110
|
+
algorithms,
|
|
111
|
+
issuer: expected.serviceIssuer,
|
|
112
|
+
subject: expected.serviceSubject,
|
|
113
|
+
audience: expected.kernelIssuer,
|
|
114
|
+
})
|
|
115
|
+
const expectedHash = await hashServiceFunctionManifest({
|
|
116
|
+
version: response.version,
|
|
117
|
+
service: response.service,
|
|
118
|
+
functions: response.functions,
|
|
119
|
+
})
|
|
120
|
+
if (verified.payload.purpose !== SERVICE_FUNCTIONS_PURPOSE) {
|
|
121
|
+
throw new Error('service function manifest credential has the wrong purpose')
|
|
122
|
+
}
|
|
123
|
+
if (verified.payload.nonce !== expected.nonce) {
|
|
124
|
+
throw new Error('service function manifest credential has the wrong nonce')
|
|
125
|
+
}
|
|
126
|
+
if (verified.payload.manifestHash !== expectedHash) {
|
|
127
|
+
throw new Error('service function manifest credential does not cover the returned manifest')
|
|
128
|
+
}
|
|
129
|
+
return response
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function hashServiceFunctionManifest(
|
|
133
|
+
manifest: ServiceFunctionManifest,
|
|
134
|
+
): Promise<string> {
|
|
135
|
+
const bytes = new TextEncoder().encode(
|
|
136
|
+
canonicalJson(ServiceFunctionManifestSchema.parse(manifest)),
|
|
137
|
+
)
|
|
138
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes)
|
|
139
|
+
return `sha256:${hex(new Uint8Array(digest))}`
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function functionBindingForManifest(
|
|
143
|
+
binding: FunctionBinding,
|
|
144
|
+
auth: 'required' | 'optional' | 'public' | undefined,
|
|
145
|
+
): FunctionBinding {
|
|
146
|
+
return { ...binding, auth: auth ?? 'required' }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function canonicalJson(value: unknown): string {
|
|
150
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value)
|
|
151
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
|
|
152
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
153
|
+
.filter(([, item]) => item !== undefined)
|
|
154
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
155
|
+
return `{${entries
|
|
156
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
|
|
157
|
+
.join(',')}}`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function hex(bytes: Uint8Array): string {
|
|
161
|
+
let value = ''
|
|
162
|
+
for (const byte of bytes) value += byte.toString(16).padStart(2, '0')
|
|
163
|
+
return value
|
|
164
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
SERVICE_FUNCTIONS_PATH,
|
|
3
|
+
SERVICE_FUNCTIONS_PURPOSE,
|
|
4
|
+
ServiceFunctionDiscoveryRequestSchema,
|
|
5
|
+
ServiceFunctionDiscoveryResponseSchema,
|
|
6
|
+
ServiceFunctionManifestEntrySchema,
|
|
7
|
+
ServiceFunctionManifestSchema,
|
|
8
|
+
hashServiceFunctionManifest,
|
|
9
|
+
signServiceFunctionManifest,
|
|
10
|
+
toServiceFunctionManifestEntries,
|
|
11
|
+
verifyServiceFunctionManifest,
|
|
12
|
+
} from './functions.js'
|
|
13
|
+
export type {
|
|
14
|
+
ServiceFunctionDiscoveryRequest,
|
|
15
|
+
ServiceFunctionDiscoveryResponse,
|
|
16
|
+
ServiceFunctionManifest,
|
|
17
|
+
ServiceFunctionManifestEntry,
|
|
18
|
+
} from './functions.js'
|