@serve.zone/interfaces 17.5.0 → 18.1.0
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/changelog.md +17 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/appstore/types.d.ts +24 -0
- package/dist_ts/appstore/types.js +76 -1
- package/dist_ts/data/hostedapp.d.ts +72 -0
- package/dist_ts/data/hostedapp.js +191 -2
- package/dist_ts/data/webpush.d.ts +15 -0
- package/dist_ts/requests/webpush.d.ts +5 -2
- package/package.json +1 -1
- package/readme.md +39 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/types.ts +109 -0
- package/ts/data/hostedapp.ts +268 -0
- package/ts/data/webpush.ts +17 -0
- package/ts/requests/webpush.ts +5 -1
package/changelog.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-07-23 - 18.1.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- add host-neutral platform OIDC and app-scoped role contracts for hosted apps
|
|
8
|
+
- Declare App Store OIDC capabilities without enabling them automatically.
|
|
9
|
+
- Bind client identity, token audience, and role assignments to an immutable app instance.
|
|
10
|
+
- Validate canonical same-origin callbacks, secret environment mappings, registrations, and audience-scoped claims.
|
|
11
|
+
- Document the shared per-instance enablement contract for Onebox and Cloudly.
|
|
12
|
+
|
|
13
|
+
## 2026-07-23 - 18.0.0
|
|
14
|
+
|
|
15
|
+
### Breaking Changes
|
|
16
|
+
|
|
17
|
+
- require durable controller mutation fences for Web Push binding synchronization and exact-owner deletion
|
|
18
|
+
- replace ID-only Web Push deletion requests with owner-scoped, fenced deletion requests
|
|
19
|
+
|
|
3
20
|
## 2026-07-23 - 17.5.0
|
|
4
21
|
|
|
5
22
|
### Features
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/interfaces',
|
|
6
|
-
version: '
|
|
6
|
+
version: '18.1.0',
|
|
7
7
|
description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IServiceSecretFile } from '../data/service.js';
|
|
2
|
+
import { type IHostedAppRoleDefinition } from '../data/hostedapp.js';
|
|
2
3
|
import type { IServiceDomainRoute, IServicePublicPortMapping, IServiceTargetPort } from '../data/serviceports.js';
|
|
3
4
|
import type { IStorageCapacityRequest, IStorageClassRequirement, TFilesystemStorageAccessMode, TObjectStorageAccessMode, TObjectStorageDelivery, TStorageReclaimPolicy, TStorageResourceKind, TStorageSnapshotMode } from '../platform/storage.js';
|
|
4
5
|
export type TAppStorePlatformRequirement = 'mongodb' | 's3' | 'clickhouse' | 'valkey' | 'mariadb';
|
|
@@ -132,6 +133,23 @@ export interface IAppStorePublishedPort {
|
|
|
132
133
|
protocol?: 'tcp' | 'udp';
|
|
133
134
|
hostIp?: string;
|
|
134
135
|
}
|
|
136
|
+
export interface IAppStorePlatformOidcEnvironmentVariables {
|
|
137
|
+
issuerUrl: string;
|
|
138
|
+
clientId: string;
|
|
139
|
+
/** The hosting platform must deliver this value through its secret environment path. */
|
|
140
|
+
clientSecret: string;
|
|
141
|
+
redirectUri: string;
|
|
142
|
+
audience: string;
|
|
143
|
+
}
|
|
144
|
+
export interface IAppStorePlatformOidcCapability {
|
|
145
|
+
/** Callback path relative to the app's canonical HTTPS origin. */
|
|
146
|
+
redirectPath: string;
|
|
147
|
+
roles: IHostedAppRoleDefinition[];
|
|
148
|
+
environmentVariables: IAppStorePlatformOidcEnvironmentVariables;
|
|
149
|
+
clientAuthenticationMethod: 'client_secret_basic' | 'client_secret_post';
|
|
150
|
+
}
|
|
151
|
+
export declare const validateAppStorePlatformOidcCapability: (capabilityArg: unknown) => string[];
|
|
152
|
+
export declare const validateAppStoreVersionPlatformOidc: (configArg: unknown) => string[];
|
|
135
153
|
export interface IAppStoreApp {
|
|
136
154
|
id: string;
|
|
137
155
|
name: string;
|
|
@@ -197,6 +215,12 @@ export interface IAppStoreVersionConfig {
|
|
|
197
215
|
storageRequests?: TAppStoreStorageRequest[];
|
|
198
216
|
publishedPorts?: IAppStorePublishedPort[];
|
|
199
217
|
platformRequirements?: IAppStorePlatformRequirements;
|
|
218
|
+
/**
|
|
219
|
+
* Declares that this workload can consume platform-managed OIDC. The host
|
|
220
|
+
* still requires an explicit per-instance administrator toggle before it
|
|
221
|
+
* creates a client, injects credentials, or emits app-scoped role claims.
|
|
222
|
+
*/
|
|
223
|
+
platformOidc?: IAppStorePlatformOidcCapability;
|
|
200
224
|
minOneboxVersion?: string;
|
|
201
225
|
minCloudlyVersion?: string;
|
|
202
226
|
appStoreVersion?: string;
|
|
@@ -1,7 +1,82 @@
|
|
|
1
|
+
import { validateHostedAppRoleDefinitions, } from '../data/hostedapp.js';
|
|
1
2
|
export const appStoreStorageFeatureIds = {
|
|
2
3
|
bindingsV1: 'storage.bindings.v1',
|
|
3
4
|
filesystemV1: 'storage.filesystem.v1',
|
|
4
5
|
objectStorageV1: 'storage.object-storage.v1',
|
|
5
6
|
objectStorageSecretFileV1: 'storage.object-storage.secret-file.v1',
|
|
6
7
|
};
|
|
7
|
-
|
|
8
|
+
const appStoreEnvKeyRegex = /^[A-Z_][A-Z0-9_]*$/;
|
|
9
|
+
export const validateAppStorePlatformOidcCapability = (capabilityArg) => {
|
|
10
|
+
if (capabilityArg === undefined)
|
|
11
|
+
return [];
|
|
12
|
+
if (!capabilityArg || typeof capabilityArg !== 'object' || Array.isArray(capabilityArg)) {
|
|
13
|
+
return ['platformOidc must be an object'];
|
|
14
|
+
}
|
|
15
|
+
const capability = capabilityArg;
|
|
16
|
+
const errors = [];
|
|
17
|
+
if (typeof capability.redirectPath !== 'string'
|
|
18
|
+
|| !capability.redirectPath.startsWith('/')
|
|
19
|
+
|| capability.redirectPath.startsWith('//')
|
|
20
|
+
|| /[\\?#\s]/.test(capability.redirectPath)
|
|
21
|
+
|| /%5c/i.test(capability.redirectPath)
|
|
22
|
+
|| new URL(capability.redirectPath, 'https://app.invalid').pathname !== capability.redirectPath) {
|
|
23
|
+
errors.push('platformOidc.redirectPath must be a canonical same-origin path without query or fragment');
|
|
24
|
+
}
|
|
25
|
+
errors.push(...validateHostedAppRoleDefinitions(capability.roles)
|
|
26
|
+
.map((errorArg) => `platformOidc.${errorArg}`));
|
|
27
|
+
const environment = capability.environmentVariables;
|
|
28
|
+
if (!environment || typeof environment !== 'object' || Array.isArray(environment)) {
|
|
29
|
+
errors.push('platformOidc.environmentVariables must be an object');
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const keys = ['issuerUrl', 'clientId', 'clientSecret', 'redirectUri', 'audience'];
|
|
33
|
+
const values = keys.map((keyArg) => environment[keyArg]);
|
|
34
|
+
keys.forEach((keyArg, indexArg) => {
|
|
35
|
+
if (typeof values[indexArg] !== 'string'
|
|
36
|
+
|| !appStoreEnvKeyRegex.test(values[indexArg])) {
|
|
37
|
+
errors.push(`platformOidc.environmentVariables.${keyArg} must be a valid environment key`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
if (new Set(values).size !== values.length) {
|
|
41
|
+
errors.push('platformOidc environment keys must be unique');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (capability.clientAuthenticationMethod !== 'client_secret_basic'
|
|
45
|
+
&& capability.clientAuthenticationMethod !== 'client_secret_post') {
|
|
46
|
+
errors.push('platformOidc.clientAuthenticationMethod is unsupported');
|
|
47
|
+
}
|
|
48
|
+
return errors;
|
|
49
|
+
};
|
|
50
|
+
export const validateAppStoreVersionPlatformOidc = (configArg) => {
|
|
51
|
+
if (!configArg || typeof configArg !== 'object' || Array.isArray(configArg)) {
|
|
52
|
+
return ['appstore config must be an object'];
|
|
53
|
+
}
|
|
54
|
+
const config = configArg;
|
|
55
|
+
const errors = validateAppStorePlatformOidcCapability(config.platformOidc);
|
|
56
|
+
if (!config.platformOidc || typeof config.platformOidc !== 'object'
|
|
57
|
+
|| Array.isArray(config.platformOidc))
|
|
58
|
+
return errors;
|
|
59
|
+
const capability = config.platformOidc;
|
|
60
|
+
if (!capability.environmentVariables || typeof capability.environmentVariables !== 'object'
|
|
61
|
+
|| Array.isArray(capability.environmentVariables))
|
|
62
|
+
return errors;
|
|
63
|
+
const oidcKeys = new Set(Object.values(capability.environmentVariables)
|
|
64
|
+
.filter((valueArg) => typeof valueArg === 'string'));
|
|
65
|
+
const ordinaryEnvKeys = Array.isArray(config.envVars)
|
|
66
|
+
? config.envVars.map((envArg) => (envArg && typeof envArg === 'object' && !Array.isArray(envArg)
|
|
67
|
+
? envArg.key
|
|
68
|
+
: undefined)).filter((valueArg) => typeof valueArg === 'string')
|
|
69
|
+
: [];
|
|
70
|
+
const secretFileKeys = Array.isArray(config.secretFiles)
|
|
71
|
+
? config.secretFiles.map((fileArg) => (fileArg && typeof fileArg === 'object' && !Array.isArray(fileArg)
|
|
72
|
+
? fileArg.sourceKey
|
|
73
|
+
: undefined)).filter((valueArg) => typeof valueArg === 'string')
|
|
74
|
+
: [];
|
|
75
|
+
for (const key of [...ordinaryEnvKeys, ...secretFileKeys]) {
|
|
76
|
+
if (oidcKeys.has(key)) {
|
|
77
|
+
errors.push(`platformOidc environment key '${key}' collides with app runtime input`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return errors;
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9hcHBzdG9yZS90eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQ0wsZ0NBQWdDLEdBRWpDLE1BQU0sc0JBQXNCLENBQUM7QUFpQzlCLE1BQU0sQ0FBQyxNQUFNLHlCQUF5QixHQUFHO0lBQ3ZDLFVBQVUsRUFBRSxxQkFBcUI7SUFDakMsWUFBWSxFQUFFLHVCQUF1QjtJQUNyQyxlQUFlLEVBQUUsMkJBQTJCO0lBQzVDLHlCQUF5QixFQUFFLHVDQUF1QztDQUMxRCxDQUFDO0FBK0pYLE1BQU0sbUJBQW1CLEdBQUcsb0JBQW9CLENBQUM7QUFFakQsTUFBTSxDQUFDLE1BQU0sc0NBQXNDLEdBQUcsQ0FDcEQsYUFBc0IsRUFDWixFQUFFO0lBQ1osSUFBSSxhQUFhLEtBQUssU0FBUztRQUFFLE9BQU8sRUFBRSxDQUFDO0lBQzNDLElBQUksQ0FBQyxhQUFhLElBQUksT0FBTyxhQUFhLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsYUFBYSxDQUFDLEVBQUUsQ0FBQztRQUN4RixPQUFPLENBQUMsZ0NBQWdDLENBQUMsQ0FBQztJQUM1QyxDQUFDO0lBQ0QsTUFBTSxVQUFVLEdBQUcsYUFBd0MsQ0FBQztJQUM1RCxNQUFNLE1BQU0sR0FBYSxFQUFFLENBQUM7SUFDNUIsSUFBSSxPQUFPLFVBQVUsQ0FBQyxZQUFZLEtBQUssUUFBUTtXQUMxQyxDQUFDLFVBQVUsQ0FBQyxZQUFZLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQztXQUN4QyxVQUFVLENBQUMsWUFBWSxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUM7V0FDeEMsVUFBVSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsWUFBWSxDQUFDO1dBQ3hDLE1BQU0sQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLFlBQVksQ0FBQztXQUNwQyxJQUFJLEdBQUcsQ0FBQyxVQUFVLENBQUMsWUFBWSxFQUFFLHFC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
|
|
@@ -1,4 +1,75 @@
|
|
|
1
1
|
export type THostedAppHostType = 'onebox' | 'cloudly' | string;
|
|
2
|
+
export declare const hostedAppOidcRoleClaim: "roles";
|
|
3
|
+
export declare const hostedAppOidcAppInstanceIdClaim: "servezone_app_instance_id";
|
|
4
|
+
export interface IHostedAppRoleDefinition {
|
|
5
|
+
/** Stable role identifier emitted in the hosted app's audience-scoped token. */
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface IHostedAppRoleAssignment {
|
|
11
|
+
/** Immutable hosted app instance identifier. Never use a mutable service name as this boundary. */
|
|
12
|
+
appInstanceId: string;
|
|
13
|
+
userId: string;
|
|
14
|
+
roleIds: string[];
|
|
15
|
+
assignedAt: number;
|
|
16
|
+
assignedByUserId: string;
|
|
17
|
+
}
|
|
18
|
+
export interface IHostedAppPlatformOidcClaims {
|
|
19
|
+
iss: string;
|
|
20
|
+
sub: string;
|
|
21
|
+
/** Mutable display name only. Never use this claim as identity authority. */
|
|
22
|
+
preferred_username: string;
|
|
23
|
+
aud: string;
|
|
24
|
+
iat: number;
|
|
25
|
+
exp: number;
|
|
26
|
+
auth_time: number;
|
|
27
|
+
nonce?: string;
|
|
28
|
+
[hostedAppOidcAppInstanceIdClaim]: string;
|
|
29
|
+
[hostedAppOidcRoleClaim]: string[];
|
|
30
|
+
}
|
|
31
|
+
interface IHostedAppPlatformOidcRegistrationBase {
|
|
32
|
+
/** Immutable OIDC client_id and ID-token audience for this hosted app. */
|
|
33
|
+
appInstanceId: string;
|
|
34
|
+
roleDefinitions: IHostedAppRoleDefinition[];
|
|
35
|
+
updatedAt: number;
|
|
36
|
+
}
|
|
37
|
+
type THostedAppRedirectUris = [string, ...string[]];
|
|
38
|
+
export type IHostedAppPlatformOidcRegistration = (IHostedAppPlatformOidcRegistrationBase & {
|
|
39
|
+
status: 'disabled';
|
|
40
|
+
redirectUris: [];
|
|
41
|
+
issuer?: never;
|
|
42
|
+
clientId?: never;
|
|
43
|
+
error?: never;
|
|
44
|
+
}) | (IHostedAppPlatformOidcRegistrationBase & {
|
|
45
|
+
status: 'enabling';
|
|
46
|
+
issuer: string;
|
|
47
|
+
clientId: string;
|
|
48
|
+
redirectUris: THostedAppRedirectUris;
|
|
49
|
+
error?: never;
|
|
50
|
+
}) | (IHostedAppPlatformOidcRegistrationBase & {
|
|
51
|
+
status: 'enabled' | 'disabling';
|
|
52
|
+
issuer: string;
|
|
53
|
+
clientId: string;
|
|
54
|
+
redirectUris: THostedAppRedirectUris;
|
|
55
|
+
error?: never;
|
|
56
|
+
}) | (IHostedAppPlatformOidcRegistrationBase & {
|
|
57
|
+
status: 'error';
|
|
58
|
+
redirectUris: string[];
|
|
59
|
+
issuer?: string;
|
|
60
|
+
clientId?: string;
|
|
61
|
+
error: string;
|
|
62
|
+
});
|
|
63
|
+
export declare const validateHostedAppRoleDefinitions: (rolesArg: unknown) => string[];
|
|
64
|
+
export declare const validateHostedAppPlatformOidcClaims: (claimsArg: unknown, expectedArg: {
|
|
65
|
+
issuer: string;
|
|
66
|
+
appInstanceId: string;
|
|
67
|
+
nonce?: string;
|
|
68
|
+
nowEpochSeconds?: number;
|
|
69
|
+
}) => string[];
|
|
70
|
+
export declare const validateHostedAppPlatformOidcRegistration: (registrationArg: unknown, expectedArg: {
|
|
71
|
+
appOrigin: string;
|
|
72
|
+
}) => string[];
|
|
2
73
|
export type THostedAppRuntimeStatus = 'starting' | 'running' | 'setupRequired' | 'degraded' | 'stopped' | 'unknown';
|
|
3
74
|
export type THostedAppBootstrapActionType = 'setupLink' | 'credentials' | 'message';
|
|
4
75
|
export type THostedAppBootstrapActionStatus = 'requested' | 'ready' | 'completed' | 'dismissed' | 'expired';
|
|
@@ -55,3 +126,4 @@ export interface IHostedAppLifecycleState extends IHostedAppLifecycleReport {
|
|
|
55
126
|
export type IHostedAppBootstrapActionRequest = Omit<IHostedAppBootstrapAction, 'id' | 'status' | 'createdAt' | 'updatedAt' | 'completedAt' | 'dismissedAt'> & {
|
|
56
127
|
id?: string;
|
|
57
128
|
};
|
|
129
|
+
export {};
|
|
@@ -1,2 +1,191 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
1
|
+
export const hostedAppOidcRoleClaim = 'roles';
|
|
2
|
+
export const hostedAppOidcAppInstanceIdClaim = 'servezone_app_instance_id';
|
|
3
|
+
const hostedAppRoleIdRegex = /^[a-z][a-z0-9:_-]{0,63}$/;
|
|
4
|
+
const hostedAppOpaqueIdRegex = /^[^\s\u0000-\u001f\u007f]{1,200}$/;
|
|
5
|
+
export const validateHostedAppRoleDefinitions = (rolesArg) => {
|
|
6
|
+
if (!Array.isArray(rolesArg) || rolesArg.length < 1 || rolesArg.length > 32) {
|
|
7
|
+
return ['roles must contain between 1 and 32 role definitions'];
|
|
8
|
+
}
|
|
9
|
+
const errors = [];
|
|
10
|
+
const roleIds = new Set();
|
|
11
|
+
rolesArg.forEach((roleArg, indexArg) => {
|
|
12
|
+
if (!roleArg || typeof roleArg !== 'object' || Array.isArray(roleArg)) {
|
|
13
|
+
errors.push(`role ${indexArg + 1} must be an object`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const role = roleArg;
|
|
17
|
+
if (typeof role.id !== 'string' || !hostedAppRoleIdRegex.test(role.id)) {
|
|
18
|
+
errors.push(`role ${indexArg + 1} id must be a stable lowercase identifier`);
|
|
19
|
+
}
|
|
20
|
+
else if (roleIds.has(role.id)) {
|
|
21
|
+
errors.push(`role id '${role.id}' is duplicated`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
roleIds.add(role.id);
|
|
25
|
+
}
|
|
26
|
+
if (typeof role.label !== 'string' || role.label.trim().length < 1 || role.label.length > 120) {
|
|
27
|
+
errors.push(`role ${indexArg + 1} label must contain between 1 and 120 characters`);
|
|
28
|
+
}
|
|
29
|
+
if (role.description !== undefined
|
|
30
|
+
&& (typeof role.description !== 'string' || role.description.length > 500)) {
|
|
31
|
+
errors.push(`role ${indexArg + 1} description must contain at most 500 characters`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return errors;
|
|
35
|
+
};
|
|
36
|
+
export const validateHostedAppPlatformOidcClaims = (claimsArg, expectedArg) => {
|
|
37
|
+
if (!claimsArg || typeof claimsArg !== 'object' || Array.isArray(claimsArg)) {
|
|
38
|
+
return ['OIDC claims must be an object'];
|
|
39
|
+
}
|
|
40
|
+
const claims = claimsArg;
|
|
41
|
+
const errors = [];
|
|
42
|
+
if (claims.iss !== expectedArg.issuer)
|
|
43
|
+
errors.push('OIDC issuer does not match');
|
|
44
|
+
if (claims.aud !== expectedArg.appInstanceId)
|
|
45
|
+
errors.push('OIDC audience does not match');
|
|
46
|
+
if (claims[hostedAppOidcAppInstanceIdClaim] !== expectedArg.appInstanceId) {
|
|
47
|
+
errors.push('OIDC app instance does not match the audience');
|
|
48
|
+
}
|
|
49
|
+
if (typeof claims.sub !== 'string' || claims.sub.length === 0) {
|
|
50
|
+
errors.push('OIDC subject is missing or malformed');
|
|
51
|
+
}
|
|
52
|
+
if (typeof claims.preferred_username !== 'string'
|
|
53
|
+
|| claims.preferred_username.trim().length === 0) {
|
|
54
|
+
errors.push('OIDC preferred username is missing or malformed');
|
|
55
|
+
}
|
|
56
|
+
const now = expectedArg.nowEpochSeconds ?? Math.floor(Date.now() / 1000);
|
|
57
|
+
if (!Number.isSafeInteger(claims.iat) || claims.iat < 0) {
|
|
58
|
+
errors.push('OIDC issued-at time is missing or malformed');
|
|
59
|
+
}
|
|
60
|
+
if (!Number.isSafeInteger(claims.exp) || claims.exp <= now) {
|
|
61
|
+
errors.push('OIDC token is expired or has malformed expiry');
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isSafeInteger(claims.auth_time)
|
|
64
|
+
|| claims.auth_time < 0
|
|
65
|
+
|| claims.auth_time > now) {
|
|
66
|
+
errors.push('OIDC authentication time is missing or malformed');
|
|
67
|
+
}
|
|
68
|
+
if (expectedArg.nonce !== undefined && claims.nonce !== expectedArg.nonce) {
|
|
69
|
+
errors.push('OIDC nonce does not match');
|
|
70
|
+
}
|
|
71
|
+
const roles = claims[hostedAppOidcRoleClaim];
|
|
72
|
+
if (!Array.isArray(roles) || roles.length < 1 || roles.length > 32) {
|
|
73
|
+
errors.push('OIDC roles must be an array with between 1 and 32 entries');
|
|
74
|
+
}
|
|
75
|
+
else if (roles.some((roleArg) => typeof roleArg !== 'string'
|
|
76
|
+
|| !hostedAppRoleIdRegex.test(roleArg))
|
|
77
|
+
|| new Set(roles).size !== roles.length) {
|
|
78
|
+
errors.push('OIDC roles contain malformed or duplicate identifiers');
|
|
79
|
+
}
|
|
80
|
+
return errors;
|
|
81
|
+
};
|
|
82
|
+
export const validateHostedAppPlatformOidcRegistration = (registrationArg, expectedArg) => {
|
|
83
|
+
if (!registrationArg || typeof registrationArg !== 'object'
|
|
84
|
+
|| Array.isArray(registrationArg)) {
|
|
85
|
+
return ['OIDC registration must be an object'];
|
|
86
|
+
}
|
|
87
|
+
const registration = registrationArg;
|
|
88
|
+
const errors = [];
|
|
89
|
+
let expectedOrigin;
|
|
90
|
+
try {
|
|
91
|
+
const parsedAppOrigin = new URL(expectedArg.appOrigin);
|
|
92
|
+
if (parsedAppOrigin.protocol !== 'https:'
|
|
93
|
+
|| parsedAppOrigin.username
|
|
94
|
+
|| parsedAppOrigin.password
|
|
95
|
+
|| parsedAppOrigin.origin !== expectedArg.appOrigin) {
|
|
96
|
+
errors.push('Expected OIDC app origin must be a canonical credential-free HTTPS origin');
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
expectedOrigin = parsedAppOrigin.origin;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
errors.push('Expected OIDC app origin must be an absolute URL origin');
|
|
104
|
+
}
|
|
105
|
+
if (typeof registration.appInstanceId !== 'string'
|
|
106
|
+
|| !hostedAppOpaqueIdRegex.test(registration.appInstanceId)) {
|
|
107
|
+
errors.push('OIDC registration appInstanceId is missing or malformed');
|
|
108
|
+
}
|
|
109
|
+
const statuses = ['disabled', 'enabling', 'enabled', 'disabling', 'error'];
|
|
110
|
+
if (typeof registration.status !== 'string' || !statuses.includes(registration.status)) {
|
|
111
|
+
errors.push('OIDC registration status is unsupported');
|
|
112
|
+
}
|
|
113
|
+
errors.push(...validateHostedAppRoleDefinitions(registration.roleDefinitions)
|
|
114
|
+
.map((errorArg) => `OIDC registration ${errorArg}`));
|
|
115
|
+
if (!Number.isSafeInteger(registration.updatedAt) || registration.updatedAt < 0) {
|
|
116
|
+
errors.push('OIDC registration updatedAt is missing or malformed');
|
|
117
|
+
}
|
|
118
|
+
const redirectUris = registration.redirectUris;
|
|
119
|
+
if (!Array.isArray(redirectUris)
|
|
120
|
+
|| redirectUris.length > 8
|
|
121
|
+
|| redirectUris.some((uriArg) => typeof uriArg !== 'string')) {
|
|
122
|
+
errors.push('OIDC registration redirectUris are malformed');
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
if (registration.status === 'disabled' && redirectUris.length !== 0) {
|
|
126
|
+
errors.push('Disabled OIDC registration must not retain redirect URIs');
|
|
127
|
+
}
|
|
128
|
+
if (registration.status !== 'disabled'
|
|
129
|
+
&& registration.status !== 'error'
|
|
130
|
+
&& redirectUris.length < 1) {
|
|
131
|
+
errors.push('Active OIDC registration requires a redirect URI');
|
|
132
|
+
}
|
|
133
|
+
if (new Set(redirectUris).size !== redirectUris.length) {
|
|
134
|
+
errors.push('OIDC registration redirect URIs must be unique');
|
|
135
|
+
}
|
|
136
|
+
for (const redirectUri of redirectUris) {
|
|
137
|
+
try {
|
|
138
|
+
const parsed = new URL(redirectUri);
|
|
139
|
+
if (parsed.protocol !== 'https:'
|
|
140
|
+
|| parsed.username
|
|
141
|
+
|| parsed.password
|
|
142
|
+
|| parsed.search
|
|
143
|
+
|| parsed.hash
|
|
144
|
+
|| parsed.toString() !== redirectUri) {
|
|
145
|
+
errors.push('OIDC registration redirect URIs must be canonical credential-free HTTPS URLs without query or fragment');
|
|
146
|
+
}
|
|
147
|
+
if (expectedOrigin && parsed.origin !== expectedOrigin) {
|
|
148
|
+
errors.push('OIDC registration redirect URIs must use the app origin');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
errors.push('OIDC registration redirect URIs must be absolute URLs');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const requiresIssuer = registration.status === 'enabling'
|
|
157
|
+
|| registration.status === 'enabled'
|
|
158
|
+
|| registration.status === 'disabling';
|
|
159
|
+
if (requiresIssuer || registration.issuer !== undefined) {
|
|
160
|
+
try {
|
|
161
|
+
const issuer = new URL(registration.issuer);
|
|
162
|
+
if (issuer.protocol !== 'https:' || issuer.username || issuer.password
|
|
163
|
+
|| issuer.search || issuer.hash) {
|
|
164
|
+
errors.push('OIDC registration issuer must be a credential-free HTTPS URL without query or fragment');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
errors.push('OIDC registration issuer must be an absolute URL');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const requiresClientId = registration.status === 'enabling'
|
|
172
|
+
|| registration.status === 'enabled'
|
|
173
|
+
|| registration.status === 'disabling';
|
|
174
|
+
if (registration.status === 'disabled'
|
|
175
|
+
&& (registration.issuer !== undefined || registration.clientId !== undefined)) {
|
|
176
|
+
errors.push('Disabled OIDC registration must not retain issuer or client metadata');
|
|
177
|
+
}
|
|
178
|
+
if (requiresClientId && registration.clientId !== registration.appInstanceId) {
|
|
179
|
+
errors.push('OIDC registration clientId must equal appInstanceId');
|
|
180
|
+
}
|
|
181
|
+
else if (registration.clientId !== undefined
|
|
182
|
+
&& registration.clientId !== registration.appInstanceId) {
|
|
183
|
+
errors.push('OIDC registration clientId must equal appInstanceId when present');
|
|
184
|
+
}
|
|
185
|
+
if (registration.status === 'error'
|
|
186
|
+
&& (typeof registration.error !== 'string' || registration.error.length < 1)) {
|
|
187
|
+
errors.push('Errored OIDC registration requires an error message');
|
|
188
|
+
}
|
|
189
|
+
return errors;
|
|
190
|
+
};
|
|
191
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaG9zdGVkYXBwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9ob3N0ZWRhcHAudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUEsTUFBTSxDQUFDLE1BQU0sc0JBQXNCLEdBQUcsT0FBZ0IsQ0FBQztBQUN2RCxNQUFNLENBQUMsTUFBTSwrQkFBK0IsR0FBRywyQkFBb0MsQ0FBQztBQXVFcEYsTUFBTSxvQkFBb0IsR0FBRywwQkFBMEIsQ0FBQztBQUN4RCxNQUFNLHNCQUFzQixHQUFHLG1DQUFtQyxDQUFDO0FBRW5FLE1BQU0sQ0FBQyxNQUFNLGdDQUFnQyxHQUFHLENBQzlDLFFBQWlCLEVBQ1AsRUFBRTtJQUNaLElBQUksQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxJQUFJLFFBQVEsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxJQUFJLFFBQVEsQ0FBQyxNQUFNLEdBQUcsRUFBRSxFQUFFLENBQUM7UUFDNUUsT0FBTyxDQUFDLHNEQUFzRCxDQUFDLENBQUM7SUFDbEUsQ0FBQztJQUNELE1BQU0sTUFBTSxHQUFhLEVBQUUsQ0FBQztJQUM1QixNQUFNLE9BQU8sR0FBRyxJQUFJLEdBQUcsRUFBVSxDQUFDO0lBQ2xDLFFBQVEsQ0FBQyxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsUUFBUSxFQUFFLEVBQUU7UUFDckMsSUFBSSxDQUFDLE9BQU8sSUFBSSxPQUFPLE9BQU8sS0FBSyxRQUFRLElBQUksS0FBSyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDO1lBQ3RFLE1BQU0sQ0FBQyxJQUFJLENBQUMsUUFBUSxRQUFRLEdBQUcsQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDO1lBQ3RELE9BQU87UUFDVCxDQUFDO1FBQ0QsTUFBTSxJQUFJLEdBQUcsT0FBa0MsQ0FBQztRQUNoRCxJQUFJLE9BQU8sSUFBSSxDQUFDLEVBQUUsS0FBSyxRQUFRLElBQUksQ0FBQyxvQkFBb0IsQ0FBQyxJQUFJLENBQUMsSUFBSSxD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
|
|
@@ -17,6 +17,21 @@ export interface IWebPushResourceOwner {
|
|
|
17
17
|
gatewayClientId: string;
|
|
18
18
|
appInstanceId: string;
|
|
19
19
|
}
|
|
20
|
+
export type TWebPushControllerIntent = 'enabled' | 'disabled' | 'deleted';
|
|
21
|
+
/**
|
|
22
|
+
* Durable controller fence for owner-scoped Web Push mutations.
|
|
23
|
+
*
|
|
24
|
+
* `id` and `epoch` are immutable for one owner. `generation` is a positive,
|
|
25
|
+
* strictly monotonic controller sequence. Reusing a generation is permitted
|
|
26
|
+
* only for the exact same operation, intent, and canonical request payload.
|
|
27
|
+
*/
|
|
28
|
+
export interface IWebPushControllerMutation {
|
|
29
|
+
id: string;
|
|
30
|
+
epoch: string;
|
|
31
|
+
generation: number;
|
|
32
|
+
operationId: string;
|
|
33
|
+
intent: TWebPushControllerIntent;
|
|
34
|
+
}
|
|
20
35
|
/** JSON form returned by the browser Push API. */
|
|
21
36
|
export interface IWebPushSubscription {
|
|
22
37
|
endpoint: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import type { IIdentity } from '../data/user.js';
|
|
3
|
-
import type { IWebPushBinding, IWebPushCredentialOneTimeSecret, IWebPushDeliveryStatus, IWebPushNotificationPayload, IWebPushResourceOwner, IWebPushServiceStatus, IWebPushSubscription, TWebPushCancellationTarget, TWebPushUrgency } from '../data/webpush.js';
|
|
3
|
+
import type { IWebPushBinding, IWebPushControllerMutation, IWebPushCredentialOneTimeSecret, IWebPushDeliveryStatus, IWebPushNotificationPayload, IWebPushResourceOwner, IWebPushServiceStatus, IWebPushSubscription, TWebPushCancellationTarget, TWebPushUrgency } from '../data/webpush.js';
|
|
4
4
|
/** Control-plane authentication. Never accepted by application delivery RPCs. */
|
|
5
5
|
export interface IWebPushControlRequestAuth {
|
|
6
6
|
identity?: IIdentity;
|
|
@@ -30,6 +30,7 @@ export interface IReq_SyncWebPushBinding extends plugins.typedrequestInterfaces.
|
|
|
30
30
|
method: 'syncWebPushBinding';
|
|
31
31
|
request: {
|
|
32
32
|
auth: IWebPushControlRequestAuth;
|
|
33
|
+
controller: IWebPushControllerMutation;
|
|
33
34
|
binding: TWebPushBindingSync;
|
|
34
35
|
};
|
|
35
36
|
response: {
|
|
@@ -43,7 +44,9 @@ export interface IReq_DeleteWebPushBinding extends plugins.typedrequestInterface
|
|
|
43
44
|
method: 'deleteWebPushBinding';
|
|
44
45
|
request: {
|
|
45
46
|
auth: IWebPushControlRequestAuth;
|
|
46
|
-
|
|
47
|
+
owner: IWebPushResourceOwner;
|
|
48
|
+
id?: string;
|
|
49
|
+
controller: IWebPushControllerMutation;
|
|
47
50
|
};
|
|
48
51
|
response: {
|
|
49
52
|
success: boolean;
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -208,6 +208,45 @@ if (webTarget && data.isHttpServiceTargetProtocol(webTarget.protocol)) {
|
|
|
208
208
|
|
|
209
209
|
`normalizeServicePortData()` returns service data with canonical `targetPorts`, domain `targetPort` refs, and `publicPortMappings` while removing legacy domain `port` fields from normalized writes.
|
|
210
210
|
|
|
211
|
+
## Hosted-App Authorization and Platform OIDC
|
|
212
|
+
|
|
213
|
+
An App Store version can declare that it supports platform-managed OpenID Connect. This is a capability declaration only: Onebox and Cloudly keep OIDC disabled until an administrator explicitly enables it for that exact app instance, and they can disable it again without changing the template.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { appstore } from '@serve.zone/interfaces';
|
|
217
|
+
|
|
218
|
+
const config: appstore.IAppStoreVersionConfig = {
|
|
219
|
+
image: 'registry.example.com/example/app:1.0.0',
|
|
220
|
+
platformOidc: {
|
|
221
|
+
redirectPath: '/auth/oidc/callback',
|
|
222
|
+
roles: [
|
|
223
|
+
{ id: 'admin', label: 'Administrator' },
|
|
224
|
+
{ id: 'user', label: 'User' },
|
|
225
|
+
],
|
|
226
|
+
environmentVariables: {
|
|
227
|
+
issuerUrl: 'SERVEZONE_PLATFORM_OIDC_ISSUER',
|
|
228
|
+
clientId: 'SERVEZONE_PLATFORM_OIDC_CLIENT_ID',
|
|
229
|
+
clientSecret: 'SERVEZONE_PLATFORM_OIDC_CLIENT_SECRET',
|
|
230
|
+
redirectUri: 'SERVEZONE_PLATFORM_OIDC_REDIRECT_URI',
|
|
231
|
+
audience: 'SERVEZONE_PLATFORM_OIDC_AUDIENCE',
|
|
232
|
+
},
|
|
233
|
+
clientAuthenticationMethod: 'client_secret_basic',
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`redirectPath` is a canonical callback path on the app's HTTPS origin. Registration validation also requires that canonical app origin explicitly and rejects cross-origin, normalized, query-bearing, fragment-bearing, or duplicate callback URLs. The five environment values are environment-key names, not credentials embedded in the manifest. A host injects the generated client secret through its secret environment path and injects the other registration values only while OIDC is enabled.
|
|
239
|
+
|
|
240
|
+
`data.IHostedAppRoleAssignment` binds a stable user subject to an immutable `appInstanceId`. That same app instance is the OIDC `client_id`, the ID-token `aud`, and the `servezone_app_instance_id` claim. Tokens include only the assigned roles for that audience; `preferred_username` is display metadata and must not be treated as identity authority.
|
|
241
|
+
|
|
242
|
+
Validate untrusted manifests, registrations, and claims with:
|
|
243
|
+
|
|
244
|
+
- `data.validateHostedAppRoleDefinitions`
|
|
245
|
+
- `data.validateHostedAppPlatformOidcRegistration`
|
|
246
|
+
- `data.validateHostedAppPlatformOidcClaims`
|
|
247
|
+
- `appstore.validateAppStorePlatformOidcCapability`
|
|
248
|
+
- `appstore.validateAppStoreVersionPlatformOidc`
|
|
249
|
+
|
|
211
250
|
## TypedRequest Contracts
|
|
212
251
|
|
|
213
252
|
Use `requests` when registering handlers with `@api.global/typedrequest` or when creating typed requests through a TypedSocket client.
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/appstore/types.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { IServiceSecretFile } from '../data/service.js';
|
|
2
|
+
import {
|
|
3
|
+
validateHostedAppRoleDefinitions,
|
|
4
|
+
type IHostedAppRoleDefinition,
|
|
5
|
+
} from '../data/hostedapp.js';
|
|
2
6
|
import type {
|
|
3
7
|
IServiceDomainRoute,
|
|
4
8
|
IServicePublicPortMapping,
|
|
@@ -178,6 +182,105 @@ export interface IAppStorePublishedPort {
|
|
|
178
182
|
hostIp?: string;
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
export interface IAppStorePlatformOidcEnvironmentVariables {
|
|
186
|
+
issuerUrl: string;
|
|
187
|
+
clientId: string;
|
|
188
|
+
/** The hosting platform must deliver this value through its secret environment path. */
|
|
189
|
+
clientSecret: string;
|
|
190
|
+
redirectUri: string;
|
|
191
|
+
audience: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface IAppStorePlatformOidcCapability {
|
|
195
|
+
/** Callback path relative to the app's canonical HTTPS origin. */
|
|
196
|
+
redirectPath: string;
|
|
197
|
+
roles: IHostedAppRoleDefinition[];
|
|
198
|
+
environmentVariables: IAppStorePlatformOidcEnvironmentVariables;
|
|
199
|
+
clientAuthenticationMethod: 'client_secret_basic' | 'client_secret_post';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const appStoreEnvKeyRegex = /^[A-Z_][A-Z0-9_]*$/;
|
|
203
|
+
|
|
204
|
+
export const validateAppStorePlatformOidcCapability = (
|
|
205
|
+
capabilityArg: unknown,
|
|
206
|
+
): string[] => {
|
|
207
|
+
if (capabilityArg === undefined) return [];
|
|
208
|
+
if (!capabilityArg || typeof capabilityArg !== 'object' || Array.isArray(capabilityArg)) {
|
|
209
|
+
return ['platformOidc must be an object'];
|
|
210
|
+
}
|
|
211
|
+
const capability = capabilityArg as Record<string, unknown>;
|
|
212
|
+
const errors: string[] = [];
|
|
213
|
+
if (typeof capability.redirectPath !== 'string'
|
|
214
|
+
|| !capability.redirectPath.startsWith('/')
|
|
215
|
+
|| capability.redirectPath.startsWith('//')
|
|
216
|
+
|| /[\\?#\s]/.test(capability.redirectPath)
|
|
217
|
+
|| /%5c/i.test(capability.redirectPath)
|
|
218
|
+
|| new URL(capability.redirectPath, 'https://app.invalid').pathname !== capability.redirectPath) {
|
|
219
|
+
errors.push('platformOidc.redirectPath must be a canonical same-origin path without query or fragment');
|
|
220
|
+
}
|
|
221
|
+
errors.push(...validateHostedAppRoleDefinitions(capability.roles)
|
|
222
|
+
.map((errorArg) => `platformOidc.${errorArg}`));
|
|
223
|
+
|
|
224
|
+
const environment = capability.environmentVariables;
|
|
225
|
+
if (!environment || typeof environment !== 'object' || Array.isArray(environment)) {
|
|
226
|
+
errors.push('platformOidc.environmentVariables must be an object');
|
|
227
|
+
} else {
|
|
228
|
+
const keys = ['issuerUrl', 'clientId', 'clientSecret', 'redirectUri', 'audience'] as const;
|
|
229
|
+
const values = keys.map((keyArg) => (environment as Record<string, unknown>)[keyArg]);
|
|
230
|
+
keys.forEach((keyArg, indexArg) => {
|
|
231
|
+
if (typeof values[indexArg] !== 'string'
|
|
232
|
+
|| !appStoreEnvKeyRegex.test(values[indexArg] as string)) {
|
|
233
|
+
errors.push(`platformOidc.environmentVariables.${keyArg} must be a valid environment key`);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
if (new Set(values).size !== values.length) {
|
|
237
|
+
errors.push('platformOidc environment keys must be unique');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (capability.clientAuthenticationMethod !== 'client_secret_basic'
|
|
241
|
+
&& capability.clientAuthenticationMethod !== 'client_secret_post') {
|
|
242
|
+
errors.push('platformOidc.clientAuthenticationMethod is unsupported');
|
|
243
|
+
}
|
|
244
|
+
return errors;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const validateAppStoreVersionPlatformOidc = (
|
|
248
|
+
configArg: unknown,
|
|
249
|
+
): string[] => {
|
|
250
|
+
if (!configArg || typeof configArg !== 'object' || Array.isArray(configArg)) {
|
|
251
|
+
return ['appstore config must be an object'];
|
|
252
|
+
}
|
|
253
|
+
const config = configArg as Record<string, unknown>;
|
|
254
|
+
const errors = validateAppStorePlatformOidcCapability(config.platformOidc);
|
|
255
|
+
if (!config.platformOidc || typeof config.platformOidc !== 'object'
|
|
256
|
+
|| Array.isArray(config.platformOidc)) return errors;
|
|
257
|
+
const capability = config.platformOidc as Record<string, unknown>;
|
|
258
|
+
if (!capability.environmentVariables || typeof capability.environmentVariables !== 'object'
|
|
259
|
+
|| Array.isArray(capability.environmentVariables)) return errors;
|
|
260
|
+
const oidcKeys = new Set(Object.values(capability.environmentVariables)
|
|
261
|
+
.filter((valueArg): valueArg is string => typeof valueArg === 'string'));
|
|
262
|
+
const ordinaryEnvKeys = Array.isArray(config.envVars)
|
|
263
|
+
? config.envVars.map((envArg) => (
|
|
264
|
+
envArg && typeof envArg === 'object' && !Array.isArray(envArg)
|
|
265
|
+
? (envArg as Record<string, unknown>).key
|
|
266
|
+
: undefined
|
|
267
|
+
)).filter((valueArg): valueArg is string => typeof valueArg === 'string')
|
|
268
|
+
: [];
|
|
269
|
+
const secretFileKeys = Array.isArray(config.secretFiles)
|
|
270
|
+
? config.secretFiles.map((fileArg) => (
|
|
271
|
+
fileArg && typeof fileArg === 'object' && !Array.isArray(fileArg)
|
|
272
|
+
? (fileArg as Record<string, unknown>).sourceKey
|
|
273
|
+
: undefined
|
|
274
|
+
)).filter((valueArg): valueArg is string => typeof valueArg === 'string')
|
|
275
|
+
: [];
|
|
276
|
+
for (const key of [...ordinaryEnvKeys, ...secretFileKeys]) {
|
|
277
|
+
if (oidcKeys.has(key)) {
|
|
278
|
+
errors.push(`platformOidc environment key '${key}' collides with app runtime input`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return errors;
|
|
282
|
+
};
|
|
283
|
+
|
|
181
284
|
export interface IAppStoreApp {
|
|
182
285
|
id: string;
|
|
183
286
|
name: string;
|
|
@@ -246,6 +349,12 @@ export interface IAppStoreVersionConfig {
|
|
|
246
349
|
storageRequests?: TAppStoreStorageRequest[];
|
|
247
350
|
publishedPorts?: IAppStorePublishedPort[];
|
|
248
351
|
platformRequirements?: IAppStorePlatformRequirements;
|
|
352
|
+
/**
|
|
353
|
+
* Declares that this workload can consume platform-managed OIDC. The host
|
|
354
|
+
* still requires an explicit per-instance administrator toggle before it
|
|
355
|
+
* creates a client, injects credentials, or emits app-scoped role claims.
|
|
356
|
+
*/
|
|
357
|
+
platformOidc?: IAppStorePlatformOidcCapability;
|
|
249
358
|
minOneboxVersion?: string;
|
|
250
359
|
minCloudlyVersion?: string;
|
|
251
360
|
appStoreVersion?: string;
|
package/ts/data/hostedapp.ts
CHANGED
|
@@ -1,5 +1,273 @@
|
|
|
1
1
|
export type THostedAppHostType = 'onebox' | 'cloudly' | string;
|
|
2
2
|
|
|
3
|
+
export const hostedAppOidcRoleClaim = 'roles' as const;
|
|
4
|
+
export const hostedAppOidcAppInstanceIdClaim = 'servezone_app_instance_id' as const;
|
|
5
|
+
|
|
6
|
+
export interface IHostedAppRoleDefinition {
|
|
7
|
+
/** Stable role identifier emitted in the hosted app's audience-scoped token. */
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface IHostedAppRoleAssignment {
|
|
14
|
+
/** Immutable hosted app instance identifier. Never use a mutable service name as this boundary. */
|
|
15
|
+
appInstanceId: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
roleIds: string[];
|
|
18
|
+
assignedAt: number;
|
|
19
|
+
assignedByUserId: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IHostedAppPlatformOidcClaims {
|
|
23
|
+
iss: string;
|
|
24
|
+
sub: string;
|
|
25
|
+
/** Mutable display name only. Never use this claim as identity authority. */
|
|
26
|
+
preferred_username: string;
|
|
27
|
+
aud: string;
|
|
28
|
+
iat: number;
|
|
29
|
+
exp: number;
|
|
30
|
+
auth_time: number;
|
|
31
|
+
nonce?: string;
|
|
32
|
+
[hostedAppOidcAppInstanceIdClaim]: string;
|
|
33
|
+
[hostedAppOidcRoleClaim]: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface IHostedAppPlatformOidcRegistrationBase {
|
|
37
|
+
/** Immutable OIDC client_id and ID-token audience for this hosted app. */
|
|
38
|
+
appInstanceId: string;
|
|
39
|
+
roleDefinitions: IHostedAppRoleDefinition[];
|
|
40
|
+
updatedAt: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type THostedAppRedirectUris = [string, ...string[]];
|
|
44
|
+
|
|
45
|
+
export type IHostedAppPlatformOidcRegistration =
|
|
46
|
+
| (IHostedAppPlatformOidcRegistrationBase & {
|
|
47
|
+
status: 'disabled';
|
|
48
|
+
redirectUris: [];
|
|
49
|
+
issuer?: never;
|
|
50
|
+
clientId?: never;
|
|
51
|
+
error?: never;
|
|
52
|
+
})
|
|
53
|
+
| (IHostedAppPlatformOidcRegistrationBase & {
|
|
54
|
+
status: 'enabling';
|
|
55
|
+
issuer: string;
|
|
56
|
+
clientId: string;
|
|
57
|
+
redirectUris: THostedAppRedirectUris;
|
|
58
|
+
error?: never;
|
|
59
|
+
})
|
|
60
|
+
| (IHostedAppPlatformOidcRegistrationBase & {
|
|
61
|
+
status: 'enabled' | 'disabling';
|
|
62
|
+
issuer: string;
|
|
63
|
+
clientId: string;
|
|
64
|
+
redirectUris: THostedAppRedirectUris;
|
|
65
|
+
error?: never;
|
|
66
|
+
})
|
|
67
|
+
| (IHostedAppPlatformOidcRegistrationBase & {
|
|
68
|
+
status: 'error';
|
|
69
|
+
redirectUris: string[];
|
|
70
|
+
issuer?: string;
|
|
71
|
+
clientId?: string;
|
|
72
|
+
error: string;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const hostedAppRoleIdRegex = /^[a-z][a-z0-9:_-]{0,63}$/;
|
|
76
|
+
const hostedAppOpaqueIdRegex = /^[^\s\u0000-\u001f\u007f]{1,200}$/;
|
|
77
|
+
|
|
78
|
+
export const validateHostedAppRoleDefinitions = (
|
|
79
|
+
rolesArg: unknown,
|
|
80
|
+
): string[] => {
|
|
81
|
+
if (!Array.isArray(rolesArg) || rolesArg.length < 1 || rolesArg.length > 32) {
|
|
82
|
+
return ['roles must contain between 1 and 32 role definitions'];
|
|
83
|
+
}
|
|
84
|
+
const errors: string[] = [];
|
|
85
|
+
const roleIds = new Set<string>();
|
|
86
|
+
rolesArg.forEach((roleArg, indexArg) => {
|
|
87
|
+
if (!roleArg || typeof roleArg !== 'object' || Array.isArray(roleArg)) {
|
|
88
|
+
errors.push(`role ${indexArg + 1} must be an object`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const role = roleArg as Record<string, unknown>;
|
|
92
|
+
if (typeof role.id !== 'string' || !hostedAppRoleIdRegex.test(role.id)) {
|
|
93
|
+
errors.push(`role ${indexArg + 1} id must be a stable lowercase identifier`);
|
|
94
|
+
} else if (roleIds.has(role.id)) {
|
|
95
|
+
errors.push(`role id '${role.id}' is duplicated`);
|
|
96
|
+
} else {
|
|
97
|
+
roleIds.add(role.id);
|
|
98
|
+
}
|
|
99
|
+
if (typeof role.label !== 'string' || role.label.trim().length < 1 || role.label.length > 120) {
|
|
100
|
+
errors.push(`role ${indexArg + 1} label must contain between 1 and 120 characters`);
|
|
101
|
+
}
|
|
102
|
+
if (role.description !== undefined
|
|
103
|
+
&& (typeof role.description !== 'string' || role.description.length > 500)) {
|
|
104
|
+
errors.push(`role ${indexArg + 1} description must contain at most 500 characters`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return errors;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const validateHostedAppPlatformOidcClaims = (
|
|
111
|
+
claimsArg: unknown,
|
|
112
|
+
expectedArg: {
|
|
113
|
+
issuer: string;
|
|
114
|
+
appInstanceId: string;
|
|
115
|
+
nonce?: string;
|
|
116
|
+
nowEpochSeconds?: number;
|
|
117
|
+
},
|
|
118
|
+
): string[] => {
|
|
119
|
+
if (!claimsArg || typeof claimsArg !== 'object' || Array.isArray(claimsArg)) {
|
|
120
|
+
return ['OIDC claims must be an object'];
|
|
121
|
+
}
|
|
122
|
+
const claims = claimsArg as Record<string, unknown>;
|
|
123
|
+
const errors: string[] = [];
|
|
124
|
+
if (claims.iss !== expectedArg.issuer) errors.push('OIDC issuer does not match');
|
|
125
|
+
if (claims.aud !== expectedArg.appInstanceId) errors.push('OIDC audience does not match');
|
|
126
|
+
if (claims[hostedAppOidcAppInstanceIdClaim] !== expectedArg.appInstanceId) {
|
|
127
|
+
errors.push('OIDC app instance does not match the audience');
|
|
128
|
+
}
|
|
129
|
+
if (typeof claims.sub !== 'string' || claims.sub.length === 0) {
|
|
130
|
+
errors.push('OIDC subject is missing or malformed');
|
|
131
|
+
}
|
|
132
|
+
if (typeof claims.preferred_username !== 'string'
|
|
133
|
+
|| claims.preferred_username.trim().length === 0) {
|
|
134
|
+
errors.push('OIDC preferred username is missing or malformed');
|
|
135
|
+
}
|
|
136
|
+
const now = expectedArg.nowEpochSeconds ?? Math.floor(Date.now() / 1000);
|
|
137
|
+
if (!Number.isSafeInteger(claims.iat) || (claims.iat as number) < 0) {
|
|
138
|
+
errors.push('OIDC issued-at time is missing or malformed');
|
|
139
|
+
}
|
|
140
|
+
if (!Number.isSafeInteger(claims.exp) || (claims.exp as number) <= now) {
|
|
141
|
+
errors.push('OIDC token is expired or has malformed expiry');
|
|
142
|
+
}
|
|
143
|
+
if (!Number.isSafeInteger(claims.auth_time)
|
|
144
|
+
|| (claims.auth_time as number) < 0
|
|
145
|
+
|| (claims.auth_time as number) > now) {
|
|
146
|
+
errors.push('OIDC authentication time is missing or malformed');
|
|
147
|
+
}
|
|
148
|
+
if (expectedArg.nonce !== undefined && claims.nonce !== expectedArg.nonce) {
|
|
149
|
+
errors.push('OIDC nonce does not match');
|
|
150
|
+
}
|
|
151
|
+
const roles = claims[hostedAppOidcRoleClaim];
|
|
152
|
+
if (!Array.isArray(roles) || roles.length < 1 || roles.length > 32) {
|
|
153
|
+
errors.push('OIDC roles must be an array with between 1 and 32 entries');
|
|
154
|
+
} else if (roles.some((roleArg) => typeof roleArg !== 'string'
|
|
155
|
+
|| !hostedAppRoleIdRegex.test(roleArg))
|
|
156
|
+
|| new Set(roles).size !== roles.length) {
|
|
157
|
+
errors.push('OIDC roles contain malformed or duplicate identifiers');
|
|
158
|
+
}
|
|
159
|
+
return errors;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const validateHostedAppPlatformOidcRegistration = (
|
|
163
|
+
registrationArg: unknown,
|
|
164
|
+
expectedArg: { appOrigin: string },
|
|
165
|
+
): string[] => {
|
|
166
|
+
if (!registrationArg || typeof registrationArg !== 'object'
|
|
167
|
+
|| Array.isArray(registrationArg)) {
|
|
168
|
+
return ['OIDC registration must be an object'];
|
|
169
|
+
}
|
|
170
|
+
const registration = registrationArg as Record<string, unknown>;
|
|
171
|
+
const errors: string[] = [];
|
|
172
|
+
let expectedOrigin: string | undefined;
|
|
173
|
+
try {
|
|
174
|
+
const parsedAppOrigin = new URL(expectedArg.appOrigin);
|
|
175
|
+
if (parsedAppOrigin.protocol !== 'https:'
|
|
176
|
+
|| parsedAppOrigin.username
|
|
177
|
+
|| parsedAppOrigin.password
|
|
178
|
+
|| parsedAppOrigin.origin !== expectedArg.appOrigin) {
|
|
179
|
+
errors.push('Expected OIDC app origin must be a canonical credential-free HTTPS origin');
|
|
180
|
+
} else {
|
|
181
|
+
expectedOrigin = parsedAppOrigin.origin;
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
errors.push('Expected OIDC app origin must be an absolute URL origin');
|
|
185
|
+
}
|
|
186
|
+
if (typeof registration.appInstanceId !== 'string'
|
|
187
|
+
|| !hostedAppOpaqueIdRegex.test(registration.appInstanceId)) {
|
|
188
|
+
errors.push('OIDC registration appInstanceId is missing or malformed');
|
|
189
|
+
}
|
|
190
|
+
const statuses = ['disabled', 'enabling', 'enabled', 'disabling', 'error'];
|
|
191
|
+
if (typeof registration.status !== 'string' || !statuses.includes(registration.status)) {
|
|
192
|
+
errors.push('OIDC registration status is unsupported');
|
|
193
|
+
}
|
|
194
|
+
errors.push(...validateHostedAppRoleDefinitions(registration.roleDefinitions)
|
|
195
|
+
.map((errorArg) => `OIDC registration ${errorArg}`));
|
|
196
|
+
if (!Number.isSafeInteger(registration.updatedAt) || (registration.updatedAt as number) < 0) {
|
|
197
|
+
errors.push('OIDC registration updatedAt is missing or malformed');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const redirectUris = registration.redirectUris;
|
|
201
|
+
if (!Array.isArray(redirectUris)
|
|
202
|
+
|| redirectUris.length > 8
|
|
203
|
+
|| redirectUris.some((uriArg) => typeof uriArg !== 'string')) {
|
|
204
|
+
errors.push('OIDC registration redirectUris are malformed');
|
|
205
|
+
} else {
|
|
206
|
+
if (registration.status === 'disabled' && redirectUris.length !== 0) {
|
|
207
|
+
errors.push('Disabled OIDC registration must not retain redirect URIs');
|
|
208
|
+
}
|
|
209
|
+
if (registration.status !== 'disabled'
|
|
210
|
+
&& registration.status !== 'error'
|
|
211
|
+
&& redirectUris.length < 1) {
|
|
212
|
+
errors.push('Active OIDC registration requires a redirect URI');
|
|
213
|
+
}
|
|
214
|
+
if (new Set(redirectUris).size !== redirectUris.length) {
|
|
215
|
+
errors.push('OIDC registration redirect URIs must be unique');
|
|
216
|
+
}
|
|
217
|
+
for (const redirectUri of redirectUris) {
|
|
218
|
+
try {
|
|
219
|
+
const parsed = new URL(redirectUri);
|
|
220
|
+
if (parsed.protocol !== 'https:'
|
|
221
|
+
|| parsed.username
|
|
222
|
+
|| parsed.password
|
|
223
|
+
|| parsed.search
|
|
224
|
+
|| parsed.hash
|
|
225
|
+
|| parsed.toString() !== redirectUri) {
|
|
226
|
+
errors.push('OIDC registration redirect URIs must be canonical credential-free HTTPS URLs without query or fragment');
|
|
227
|
+
}
|
|
228
|
+
if (expectedOrigin && parsed.origin !== expectedOrigin) {
|
|
229
|
+
errors.push('OIDC registration redirect URIs must use the app origin');
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
errors.push('OIDC registration redirect URIs must be absolute URLs');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const requiresIssuer = registration.status === 'enabling'
|
|
238
|
+
|| registration.status === 'enabled'
|
|
239
|
+
|| registration.status === 'disabling';
|
|
240
|
+
if (requiresIssuer || registration.issuer !== undefined) {
|
|
241
|
+
try {
|
|
242
|
+
const issuer = new URL(registration.issuer as string);
|
|
243
|
+
if (issuer.protocol !== 'https:' || issuer.username || issuer.password
|
|
244
|
+
|| issuer.search || issuer.hash) {
|
|
245
|
+
errors.push('OIDC registration issuer must be a credential-free HTTPS URL without query or fragment');
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
errors.push('OIDC registration issuer must be an absolute URL');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const requiresClientId = registration.status === 'enabling'
|
|
252
|
+
|| registration.status === 'enabled'
|
|
253
|
+
|| registration.status === 'disabling';
|
|
254
|
+
if (registration.status === 'disabled'
|
|
255
|
+
&& (registration.issuer !== undefined || registration.clientId !== undefined)) {
|
|
256
|
+
errors.push('Disabled OIDC registration must not retain issuer or client metadata');
|
|
257
|
+
}
|
|
258
|
+
if (requiresClientId && registration.clientId !== registration.appInstanceId) {
|
|
259
|
+
errors.push('OIDC registration clientId must equal appInstanceId');
|
|
260
|
+
} else if (registration.clientId !== undefined
|
|
261
|
+
&& registration.clientId !== registration.appInstanceId) {
|
|
262
|
+
errors.push('OIDC registration clientId must equal appInstanceId when present');
|
|
263
|
+
}
|
|
264
|
+
if (registration.status === 'error'
|
|
265
|
+
&& (typeof registration.error !== 'string' || registration.error.length < 1)) {
|
|
266
|
+
errors.push('Errored OIDC registration requires an error message');
|
|
267
|
+
}
|
|
268
|
+
return errors;
|
|
269
|
+
};
|
|
270
|
+
|
|
3
271
|
export type THostedAppRuntimeStatus =
|
|
4
272
|
| 'starting'
|
|
5
273
|
| 'running'
|
package/ts/data/webpush.ts
CHANGED
|
@@ -33,6 +33,23 @@ export interface IWebPushResourceOwner {
|
|
|
33
33
|
appInstanceId: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export type TWebPushControllerIntent = 'enabled' | 'disabled' | 'deleted';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Durable controller fence for owner-scoped Web Push mutations.
|
|
40
|
+
*
|
|
41
|
+
* `id` and `epoch` are immutable for one owner. `generation` is a positive,
|
|
42
|
+
* strictly monotonic controller sequence. Reusing a generation is permitted
|
|
43
|
+
* only for the exact same operation, intent, and canonical request payload.
|
|
44
|
+
*/
|
|
45
|
+
export interface IWebPushControllerMutation {
|
|
46
|
+
id: string;
|
|
47
|
+
epoch: string;
|
|
48
|
+
generation: number;
|
|
49
|
+
operationId: string;
|
|
50
|
+
intent: TWebPushControllerIntent;
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
/** JSON form returned by the browser Push API. */
|
|
37
54
|
export interface IWebPushSubscription {
|
|
38
55
|
endpoint: string;
|
package/ts/requests/webpush.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
|
|
|
2
2
|
import type { IIdentity } from '../data/user.js';
|
|
3
3
|
import type {
|
|
4
4
|
IWebPushBinding,
|
|
5
|
+
IWebPushControllerMutation,
|
|
5
6
|
IWebPushCredentialOneTimeSecret,
|
|
6
7
|
IWebPushDeliveryStatus,
|
|
7
8
|
IWebPushNotificationPayload,
|
|
@@ -54,6 +55,7 @@ export interface IReq_SyncWebPushBinding extends plugins.typedrequestInterfaces.
|
|
|
54
55
|
method: 'syncWebPushBinding';
|
|
55
56
|
request: {
|
|
56
57
|
auth: IWebPushControlRequestAuth;
|
|
58
|
+
controller: IWebPushControllerMutation;
|
|
57
59
|
binding: TWebPushBindingSync;
|
|
58
60
|
};
|
|
59
61
|
response: {
|
|
@@ -71,7 +73,9 @@ export interface IReq_DeleteWebPushBinding extends plugins.typedrequestInterface
|
|
|
71
73
|
method: 'deleteWebPushBinding';
|
|
72
74
|
request: {
|
|
73
75
|
auth: IWebPushControlRequestAuth;
|
|
74
|
-
|
|
76
|
+
owner: IWebPushResourceOwner;
|
|
77
|
+
id?: string;
|
|
78
|
+
controller: IWebPushControllerMutation;
|
|
75
79
|
};
|
|
76
80
|
response: {
|
|
77
81
|
success: boolean;
|