sos-sdk-node-ts 0.1.13 → 0.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.1.14 - 2026-10-21
6
+
7
+ - Add a product-neutral server-profile owner assertion for personal passkey
8
+ registration. It binds the selected profile to the authenticated Firebase
9
+ principal without misinterpreting an individual-member actor DID as a
10
+ professional employee identifier; mandatory passkey enrollment after PIN
11
+ verification remains unchanged.
12
+ - Accept the browser WebAuthn secure-context exception for local loopback
13
+ origins while continuing to reject plain HTTP origins on remote hosts.
14
+
15
+ - Adopt the product-neutral secure organization registration runtime previously
16
+ specialized in the clinical Node SDK: reviewed controller bootstrap-wallet
17
+ reconstruction, encrypted verification/reissuance transport, Offer-to-Order
18
+ confirmation and terminal OperationOutcome rejection. Portal forms, sector
19
+ admission policy, profile persistence and deployment values remain outside
20
+ this package.
21
+
3
22
  ## 0.1.13 - 2026-10-21
4
23
 
5
24
  - Add the trusted-BFF host organization directory client. It submits and polls
package/README.md CHANGED
@@ -24,6 +24,15 @@ begins only after exact registry packages are installed and verified.
24
24
 
25
25
  Product-neutral Node adapters for trusted SOSCHAIN runtimes.
26
26
 
27
+ `secure-organization-registration` owns the trusted Node transport used after
28
+ an application has been reviewed: deterministic reconstruction of the exact
29
+ controller bootstrap wallet, encrypted organization verification or credential
30
+ reissuance, and confirmation of the exact accepted Offer through Order. It
31
+ does not own portal forms, admission policy, user authentication, application
32
+ storage, payment policy or professional operations.
33
+ The capability-by-capability source decision is recorded in
34
+ [`docs/ORGANIZATION_REGISTRATION_SOURCE_EVALUATION.md`](docs/ORGANIZATION_REGISTRATION_SOURCE_EVALUATION.md).
35
+
27
36
  ```text
28
37
  gdc-* frozen
29
38
  ↓
package/dist/index.d.ts CHANGED
@@ -9,3 +9,4 @@ export * from './bff-email-assertion.js';
9
9
  export * from './subject-card-directory-client.js';
10
10
  export * from './server-passkey-profile.js';
11
11
  export * from './host-organization-directory-client.js';
12
+ export * from './secure-organization-registration.js';
package/dist/index.js CHANGED
@@ -9,3 +9,4 @@ export * from './bff-email-assertion.js';
9
9
  export * from './subject-card-directory-client.js';
10
10
  export * from './server-passkey-profile.js';
11
11
  export * from './host-organization-directory-client.js';
12
+ export * from './secure-organization-registration.js';
@@ -0,0 +1,58 @@
1
+ import { resolveDidWebKeyAgreementJwk, type AppInfo, type HostRouteContext, type LegalOrganizationOrderInput, type NodeLegalOrganizationVerificationTransactionInput, type PollOptions, type RouteContext, type SubmitAndPollResult } from 'gdc-sdk-node-ts';
2
+ type PublicJwk = Readonly<Record<string, unknown>>;
3
+ export type HostScopedDidWebResolverOptions = Readonly<{
4
+ gatewayBaseUrl: string;
5
+ hostCoverageScope: string;
6
+ version: string;
7
+ hostNetwork: string;
8
+ fetchImpl?: typeof fetch;
9
+ }>;
10
+ export declare function buildHostScopedDidDocumentUrl(options: Omit<HostScopedDidWebResolverOptions, 'fetchImpl'>): string;
11
+ export declare function createHostScopedDidWebKeyAgreementResolver(options: HostScopedDidWebResolverOptions): (recipientDid: string) => ReturnType<typeof resolveDidWebKeyAgreementJwk>;
12
+ export type SecureOrganizationRegistrationRuntimeOptions = Readonly<{
13
+ gatewayBaseUrl: string;
14
+ appInfo?: AppInfo;
15
+ resolveRecipientJwk(recipientDid: string): Promise<PublicJwk>;
16
+ fetchImpl?: typeof fetch;
17
+ requestTimeoutMs?: number;
18
+ }>;
19
+ type SecureOrganizationBootstrapInput = Readonly<{
20
+ walletSeed: string;
21
+ walletKeyDerivationId: string;
22
+ bearerToken: string;
23
+ providerDid: string;
24
+ routeContext: RouteContext;
25
+ hostContext: HostRouteContext;
26
+ pollOptions?: PollOptions;
27
+ }>;
28
+ export type SecureOrganizationRegistrationInput = SecureOrganizationBootstrapInput & Readonly<{
29
+ verificationInput: NodeLegalOrganizationVerificationTransactionInput;
30
+ }>;
31
+ export type SecureOrganizationOrderInput = SecureOrganizationBootstrapInput & Readonly<{
32
+ orderInput: LegalOrganizationOrderInput;
33
+ }>;
34
+ export declare function buildOrganizationRegistrationWalletContext(walletKeyDerivationId: string): {
35
+ readonly profile: {
36
+ readonly profileId: string;
37
+ };
38
+ readonly runtime: {
39
+ readonly runtimeId: `${string}:server-runtime`;
40
+ readonly runtimeType: "backend-service";
41
+ };
42
+ };
43
+ /** Trusted-server facade for encrypted verification, reissuance and Offer-to-Order confirmation. */
44
+ export declare class SecureOrganizationRegistrationRuntime {
45
+ private readonly options;
46
+ constructor(options: SecureOrganizationRegistrationRuntimeOptions);
47
+ submitVerificationWithBootstrapWallet(input: SecureOrganizationRegistrationInput): Promise<SubmitAndPollResult>;
48
+ submitCredentialReissuanceWithBootstrapWallet(input: SecureOrganizationRegistrationInput): Promise<SubmitAndPollResult>;
49
+ confirmOrderWithBootstrapWallet(input: SecureOrganizationOrderInput): Promise<SubmitAndPollResult>;
50
+ private createEncryptedClient;
51
+ }
52
+ export declare function assertSuccessfulOrganizationRegistrationResult<T extends Readonly<{
53
+ poll: Readonly<{
54
+ status: number;
55
+ body: unknown;
56
+ }>;
57
+ }>>(result: T): T;
58
+ export {};
@@ -0,0 +1,128 @@
1
+ // Copyright 2026 Connecting Solution & Applications Ltd under the Apache License, Version 2.0.
2
+ import { NodeHttpClient, NodeManagedWallet, TransportProfiles, resolveDidWebKeyAgreementJwk, } from 'gdc-sdk-node-ts';
3
+ import { buildAuthorityDidWeb } from 'gdc-common-utils-ts/utils/authority-resolution';
4
+ export function buildHostScopedDidDocumentUrl(options) {
5
+ const gatewayOrigin = secureOrigin(options.gatewayBaseUrl, 'host_did_gateway_url_required');
6
+ const coverage = routeSegment(options.hostCoverageScope, 'host_did_coverage_scope_required').toUpperCase();
7
+ const version = routeSegment(options.version, 'host_did_version_required');
8
+ const network = routeSegment(options.hostNetwork, 'host_did_network_required');
9
+ return `${gatewayOrigin}/host/cds-${coverage}/${version}/${network}/.well-known/did.json`;
10
+ }
11
+ export function createHostScopedDidWebKeyAgreementResolver(options) {
12
+ const gatewayOrigin = secureOrigin(options.gatewayBaseUrl, 'host_did_gateway_url_required');
13
+ const hostDid = buildAuthorityDidWeb({ authority: gatewayOrigin }).toLowerCase();
14
+ const didDocumentUrl = buildHostScopedDidDocumentUrl(options);
15
+ return recipientDid => resolveDidWebKeyAgreementJwk(recipientDid, {
16
+ fetchImpl: options.fetchImpl,
17
+ ...(String(recipientDid || '').trim().toLowerCase() === hostDid ? { didDocumentUrl } : {}),
18
+ });
19
+ }
20
+ export function buildOrganizationRegistrationWalletContext(walletKeyDerivationId) {
21
+ const id = required(walletKeyDerivationId, 'organization_registration_wallet_context_required');
22
+ return { profile: { profileId: id }, runtime: { runtimeId: `${id}:server-runtime`, runtimeType: 'backend-service' } };
23
+ }
24
+ /** Trusted-server facade for encrypted verification, reissuance and Offer-to-Order confirmation. */
25
+ export class SecureOrganizationRegistrationRuntime {
26
+ options;
27
+ constructor(options) {
28
+ this.options = options;
29
+ secureOrigin(options.gatewayBaseUrl, 'organization_registration_gateway_url_required');
30
+ }
31
+ async submitVerificationWithBootstrapWallet(input) {
32
+ const client = await this.createEncryptedClient(input);
33
+ return assertSuccessfulOrganizationRegistrationResult(await client.submitLegalOrganizationVerificationTransaction(input.hostContext, input.verificationInput, input.pollOptions));
34
+ }
35
+ async submitCredentialReissuanceWithBootstrapWallet(input) {
36
+ const client = await this.createEncryptedClient(input);
37
+ return assertSuccessfulOrganizationRegistrationResult(await client.submitLegalOrganizationCredentialReissuance(input.hostContext, input.verificationInput, input.pollOptions));
38
+ }
39
+ async confirmOrderWithBootstrapWallet(input) {
40
+ const client = await this.createEncryptedClient(input);
41
+ return assertSuccessfulOrganizationRegistrationResult(await client.confirmLegalOrganizationOrder(input.hostContext, input.orderInput, input.pollOptions));
42
+ }
43
+ async createEncryptedClient(input) {
44
+ const walletKeyDerivationId = required(input.walletKeyDerivationId, 'organization_registration_wallet_context_required');
45
+ const walletSeed = required(input.walletSeed, 'organization_registration_wallet_seed_required');
46
+ const providerDid = required(input.providerDid, 'organization_registration_provider_did_required');
47
+ const bearerToken = required(input.bearerToken, 'organization_registration_bearer_required');
48
+ const wallet = new NodeManagedWallet({ resolveRecipientJwk: this.options.resolveRecipientJwk });
49
+ const context = buildOrganizationRegistrationWalletContext(walletKeyDerivationId);
50
+ await wallet.provisionManagedKeys(context, { ownerScope: 'profile', purposes: ['actor-signing', 'document-at-rest'], mode: 'deterministic', seedMaterial: walletSeed });
51
+ await wallet.provisionManagedKeys(context, { ownerScope: 'runtime', purposes: ['openid-id-token-signing', 'vp-token-signing', 'comm-signing', 'comm-encryption'], mode: 'deterministic', seedMaterial: walletSeed });
52
+ return new NodeHttpClient({
53
+ baseUrl: this.options.gatewayBaseUrl, bearerToken, ctx: input.routeContext,
54
+ appInfo: this.options.appInfo, fetchImpl: this.options.fetchImpl, requestTimeoutMs: this.options.requestTimeoutMs,
55
+ transportProfile: TransportProfiles.DidcommEncryptedForm,
56
+ secureTransportAdapter: {
57
+ pack: message => wallet.packForRecipientWithContext(message, providerDid, { context }),
58
+ unpack: async (jwe) => (await wallet.unpackWithContext(jwe, { context })).content,
59
+ },
60
+ });
61
+ }
62
+ }
63
+ export function assertSuccessfulOrganizationRegistrationResult(result) {
64
+ const failure = findFailedOperationOutcome(result.poll.body);
65
+ if (result.poll.status >= 400 || failure) {
66
+ const code = String(failure?.issue.code || 'operation-outcome').trim() || 'operation-outcome';
67
+ const status = failure?.status || result.poll.status;
68
+ throw new Error(`organization_registration_gateway_failed:${status}:${code}`);
69
+ }
70
+ return result;
71
+ }
72
+ function findFailedOperationOutcome(value) {
73
+ const seen = new WeakSet();
74
+ const visit = (candidate, inheritedStatus) => {
75
+ if (!candidate || typeof candidate !== 'object' || seen.has(candidate))
76
+ return undefined;
77
+ seen.add(candidate);
78
+ if (Array.isArray(candidate)) {
79
+ for (const item of candidate) {
80
+ const failure = visit(item, inheritedStatus);
81
+ if (failure)
82
+ return failure;
83
+ }
84
+ return undefined;
85
+ }
86
+ const record = asRecord(candidate);
87
+ const responseStatus = parseHttpStatus(asRecord(record.response).status) || inheritedStatus;
88
+ if (record.resourceType === 'OperationOutcome') {
89
+ const issues = Array.isArray(record.issue) ? record.issue.map(asRecord) : [];
90
+ const issue = issues.find(item => ['error', 'fatal'].includes(String(item.severity || '').toLowerCase()));
91
+ if (issue)
92
+ return { issue, status: parseHttpStatus(issue.responseStatus) || responseStatus };
93
+ }
94
+ for (const nested of Object.values(record)) {
95
+ const failure = visit(nested, responseStatus);
96
+ if (failure)
97
+ return failure;
98
+ }
99
+ return undefined;
100
+ };
101
+ return visit(value);
102
+ }
103
+ function parseHttpStatus(value) {
104
+ const status = Number.parseInt(String(value ?? ''), 10);
105
+ return Number.isInteger(status) && status >= 100 && status <= 599 ? status : undefined;
106
+ }
107
+ function asRecord(value) {
108
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
109
+ }
110
+ function required(value, error) {
111
+ const normalized = String(value ?? '').trim();
112
+ if (!normalized)
113
+ throw new TypeError(error);
114
+ return normalized;
115
+ }
116
+ function routeSegment(value, error) {
117
+ const normalized = required(value, error);
118
+ if (!/^[A-Za-z0-9-]+$/u.test(normalized))
119
+ throw new TypeError(error);
120
+ return normalized;
121
+ }
122
+ function secureOrigin(value, error) {
123
+ const url = new URL(required(value, error));
124
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '::1'].includes(url.hostname))) {
125
+ throw new TypeError('organization_registration_gateway_https_required');
126
+ }
127
+ return url.origin;
128
+ }
@@ -102,6 +102,14 @@ export type ServerPasskeyProfileServiceOptions = Readonly<{
102
102
  ceremonyTtlMs?: number;
103
103
  accessTtlMs?: number;
104
104
  }>;
105
+ /**
106
+ * Confirms that an authenticated Firebase principal owns the selected server
107
+ * profile. Personal actor DIDs describe the subject relationship and are not
108
+ * professional employee identifiers, so they must not be parsed as one.
109
+ */
110
+ export declare function assertServerProfileOwner<T extends Readonly<{
111
+ ownerId: string;
112
+ }>>(profile: T | undefined, ownerId: string): asserts profile is T;
105
113
  /** Product-neutral passkey state machine for SOS, health and animal portal BFFs. */
106
114
  export declare class ServerPasskeyProfileService {
107
115
  private readonly options;
@@ -2,6 +2,17 @@ import { createHash, randomBytes, randomUUID } from 'node:crypto';
2
2
  import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from '@simplewebauthn/server';
3
3
  const CEREMONY_TTL_MS = 5 * 60 * 1000;
4
4
  const ACCESS_TTL_MS = 15 * 60 * 1000;
5
+ /**
6
+ * Confirms that an authenticated Firebase principal owns the selected server
7
+ * profile. Personal actor DIDs describe the subject relationship and are not
8
+ * professional employee identifiers, so they must not be parsed as one.
9
+ */
10
+ export function assertServerProfileOwner(profile, ownerId) {
11
+ const expectedOwnerId = ownerId.trim();
12
+ if (!profile || !expectedOwnerId || profile.ownerId !== expectedOwnerId) {
13
+ throw new Error('profile_not_found');
14
+ }
15
+ }
5
16
  /** Product-neutral passkey state machine for SOS, health and animal portal BFFs. */
6
17
  export class ServerPasskeyProfileService {
7
18
  options;
@@ -140,7 +151,7 @@ export function createSimpleWebAuthnCeremonyAdapter(input) {
140
151
  const rpName = String(input.rpName || '').trim();
141
152
  const rpId = String(input.rpId || '').trim();
142
153
  const origins = input.origins.map(value => String(value || '').trim()).filter(Boolean);
143
- if (!rpName || !rpId || !origins.length || origins.some(origin => !origin.startsWith('https://'))) {
154
+ if (!rpName || !rpId || !origins.length || origins.some(origin => !isSecureWebAuthnOrigin(origin))) {
144
155
  throw new Error('passkey_relying_party_not_configured');
145
156
  }
146
157
  return {
@@ -204,3 +215,15 @@ export function createSimpleWebAuthnCeremonyAdapter(input) {
204
215
  },
205
216
  };
206
217
  }
218
+ function isSecureWebAuthnOrigin(origin) {
219
+ try {
220
+ const parsed = new URL(origin);
221
+ if (parsed.protocol === 'https:')
222
+ return true;
223
+ return parsed.protocol === 'http:'
224
+ && (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname === '[::1]');
225
+ }
226
+ catch {
227
+ return false;
228
+ }
229
+ }
@@ -0,0 +1,27 @@
1
+ # Organization registration source evaluation
2
+
3
+ The shared SOS Node SDK evaluated the existing clinical organization
4
+ registration runtime capability-by-capability. It did not copy a portal,
5
+ product policy or deployment configuration.
6
+
7
+ | Capability | Decision | Shared boundary |
8
+ | --- | --- | --- |
9
+ | deterministic controller bootstrap-wallet reconstruction | adopted | trusted Node SDK |
10
+ | encrypted DIDComm form transport to SOS Gateway | adopted | trusted Node SDK |
11
+ | organization verification and credential reissuance submission | adopted | trusted Node SDK |
12
+ | exact accepted Offer continuation through Order | adopted | trusted Node SDK |
13
+ | terminal nested `OperationOutcome` rejection | adopted | trusted Node SDK |
14
+ | host DID document route and recipient key resolution | adopted | trusted Node SDK |
15
+ | host organization search and lifecycle | already equivalent | existing host-directory client |
16
+ | application form, localization and product branding | rejected | hosting/controller presentation |
17
+ | sector and jurisdiction admission policy | rejected | trusted portal BFF and ICA |
18
+ | application, PIN and controller-profile persistence | rejected | trusted portal BFF |
19
+ | user authentication and account recovery | rejected | portal identity boundary |
20
+ | payment adapter and commercial policy | rejected | marketplace and billing boundary |
21
+ | professional groups, subjects, services, studies and scheduling | rejected | delegated professional workflows |
22
+ | tenant, host, network, credentials and deployment values | rejected | private deployment configuration |
23
+
24
+ This extraction is a transport prerequisite, not proof of an organization
25
+ onboarding journey. A consumer still has to prove application submission,
26
+ review, verification, Offer, Order, activation, device registration and
27
+ authoritative readback through its real BFF and Gateway environment.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sos-sdk-node-ts",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Product-neutral Node adapters shared by SOSChain, Vet and UHC runtimes",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",
@@ -51,6 +51,10 @@
51
51
  "./host-organization-directory-client": {
52
52
  "types": "./dist/host-organization-directory-client.d.ts",
53
53
  "default": "./dist/host-organization-directory-client.js"
54
+ },
55
+ "./secure-organization-registration": {
56
+ "types": "./dist/secure-organization-registration.d.ts",
57
+ "default": "./dist/secure-organization-registration.js"
54
58
  }
55
59
  },
56
60
  "files": [
@@ -70,6 +74,8 @@
70
74
  },
71
75
  "dependencies": {
72
76
  "@simplewebauthn/server": "13.3.2",
77
+ "gdc-common-utils-ts": "2.9.30",
78
+ "gdc-sdk-node-ts": "2.9.32",
73
79
  "sos-data-utils-ts": "0.6.13",
74
80
  "sos-sdk-core-ts": "0.5.2"
75
81
  },