@learncard/lca-api-service 1.3.2 → 1.5.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/.env.example +99 -2
- package/AGENTS.md +155 -44
- package/CHANGELOG.md +84 -0
- package/bootstrapLambda.test.ts +76 -0
- package/compose.env +5 -0
- package/config/config.dev.json +23 -0
- package/config/config.example.json +22 -0
- package/config/config.json +1 -0
- package/config/config.production.json +12 -0
- package/config/config.scouts.dev.json +11 -0
- package/config/config.scouts.production.json +11 -0
- package/dist/accesslayer/signing-authority/index.d.ts +8 -2
- package/dist/app.d.ts +361 -8
- package/dist/cache/challenges.d.ts +2 -0
- package/dist/cache/getDel.d.ts +2 -0
- package/dist/cache/getDel.test.d.ts +1 -0
- package/dist/cache/login-tickets.d.ts +12 -0
- package/dist/cache/recoverySessions.d.ts +42 -0
- package/dist/config/applyDockerStageConfig.d.ts +1 -0
- package/dist/config/cacheEnvironment.d.ts +16 -0
- package/dist/config/environment.d.ts +253 -2
- package/dist/config/environment.test.d.ts +1 -0
- package/dist/config/escrowBundleMode.test.d.ts +1 -0
- package/dist/config/escrowDeployment.test.d.ts +1 -0
- package/dist/config/oidcEnvironment.d.ts +74 -0
- package/dist/config/serverlessFunctionEnv.test.d.ts +1 -0
- package/dist/config/stageConfig.d.ts +62 -0
- package/dist/config/stageConfig.test.d.ts +1 -0
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/docker-entry.d.ts +1 -1
- package/dist/helpers/auth.helpers.d.ts +1 -1
- package/dist/helpers/escrowCancelLink.d.ts +7 -0
- package/dist/helpers/escrowCancelLink.test.d.ts +1 -0
- package/dist/helpers/escrowCancelToken.d.ts +4 -0
- package/dist/helpers/escrowCancelToken.test.d.ts +1 -0
- package/dist/helpers/keycloak.helpers.d.ts +23 -0
- package/dist/helpers/learnCard.signingAuthority.test.d.ts +1 -0
- package/dist/helpers/login-ticket.helpers.d.ts +31 -0
- package/dist/helpers/login-ticket.helpers.test.d.ts +1 -0
- package/dist/helpers/oidc.helpers.d.ts +47 -0
- package/dist/helpers/rate-limit.helpers.d.ts +6 -0
- package/dist/helpers/rate-limit.helpers.test.d.ts +1 -0
- package/dist/helpers/rateLimit.helpers.d.ts +4 -0
- package/dist/helpers/seedEncryption.helpers.d.ts +23 -0
- package/dist/helpers/seedEncryption.helpers.test.d.ts +1 -0
- package/dist/helpers/social-token.helpers.d.ts +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/jobs/escrowBlobRewrap.d.ts +9 -0
- package/dist/jobs/escrowHoldReminders.d.ts +12 -0
- package/dist/migrations/signingAuthoritySeeds.d.ts +13 -0
- package/dist/migrations/signingAuthoritySeedsCli.test.d.ts +1 -0
- package/dist/models/AuthSubject.d.ts +20 -0
- package/dist/models/EscrowHold.d.ts +153 -0
- package/dist/models/Notification.d.ts +5 -0
- package/dist/models/SigningAuthority.d.ts +7 -1
- package/dist/models/UserKey.d.ts +326 -3
- package/dist/models/UserKey.escrowPin.test.d.ts +1 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/oidc.d.ts +5 -0
- package/dist/routes/auth.d.ts +31 -0
- package/dist/routes/escrow.d.ts +164 -0
- package/dist/routes/index.d.ts +6 -0
- package/dist/routes/keys.d.ts +182 -7
- package/dist/routes/notifications.d.ts +5 -0
- package/dist/routes/qr-login.d.ts +1 -1
- package/dist/routes/signing-authority.d.ts +14 -0
- package/dist/services/escrow-enclave/blobIdentity.d.ts +10 -0
- package/dist/services/escrow-enclave/blobIdentity.test.d.ts +1 -0
- package/dist/services/escrow-enclave/cancellation.d.ts +3 -0
- package/dist/services/escrow-enclave/enclaveTrustAnchors.d.ts +1 -0
- package/dist/services/escrow-enclave/index.d.ts +25 -0
- package/dist/services/escrow-enclave/notifications.d.ts +24 -0
- package/dist/services/escrow-enclave/notifications.test.d.ts +1 -0
- package/dist/services/escrow-enclave/production.test.d.ts +1 -0
- package/dist/services/escrow-enclave/remoteEnclave.contract.test.d.ts +1 -0
- package/dist/services/escrow-enclave/remoteEnclave.d.ts +20 -0
- package/dist/services/escrow-enclave/remoteEnclave.test.d.ts +1 -0
- package/dist/services/escrow-enclave/remoteEnclave.tls.test.d.ts +1 -0
- package/dist/services/escrow-enclave/softwareEnclave.d.ts +27 -0
- package/dist/services/escrow-enclave/softwareEnclave.test.d.ts +1 -0
- package/dist/services/escrow-enclave/types.d.ts +111 -0
- package/dist/types/notifications.d.ts +5 -0
- package/dist/types/seed-encryption.d.ts +31 -0
- package/dist/types/seed-migration.d.ts +49 -0
- package/esbuildPlugins.cjs +1 -0
- package/lambda.ts +27 -112
- package/lambdaApp.ts +146 -0
- package/oidcLambda.test.ts +38 -0
- package/oidcLambda.ts +18 -0
- package/oidcLambdaApp.ts +11 -0
- package/package.json +30 -14
- package/scripts/README.md +88 -0
- package/scripts/check-seed-encryption-infrastructure.mjs +139 -0
- package/scripts/check-signing-authority-redis.mjs +93 -0
- package/scripts/escrow-blob-mode-report.test.ts +227 -0
- package/scripts/escrow-blob-mode-report.ts +465 -0
- package/scripts/keycloak-admin.ts +108 -0
- package/scripts/keycloak-provider-links.ts +147 -0
- package/scripts/migrate-signing-authority-seeds-local.sh +51 -0
- package/scripts/migrate-signing-authority-seeds.mjs +105 -0
- package/scripts/print-seed-rollout-flags.ts +21 -0
- package/scripts/provision-keycloak-users.ts +419 -0
- package/seedMigrationApp.test.ts +145 -0
- package/seedMigrationApp.ts +50 -0
- package/seedMigrationLambda.test.ts +40 -0
- package/seedMigrationLambda.ts +19 -0
- package/serverless-local.yml +28 -1
- package/serverless.function-env.cjs +58 -0
- package/serverless.yml +294 -26
- package/src/accesslayer/notifications/index.ts +9 -2
- package/src/accesslayer/signing-authority/create.ts +11 -7
- package/src/accesslayer/signing-authority/index.ts +4 -1
- package/src/app.ts +4 -0
- package/src/cache/challenges.ts +12 -2
- package/src/cache/getDel.test.ts +34 -0
- package/src/cache/getDel.ts +32 -0
- package/src/cache/in-memory-lru.ts +1 -0
- package/src/cache/index.ts +1 -1
- package/src/cache/login-tickets.ts +26 -0
- package/src/cache/recoverySessions.ts +252 -0
- package/src/config/applyDockerStageConfig.ts +6 -0
- package/src/config/cacheEnvironment.ts +34 -0
- package/src/config/environment.test.ts +159 -0
- package/src/config/environment.ts +164 -0
- package/src/config/escrowBundleMode.test.ts +52 -0
- package/src/config/escrowDeployment.test.ts +114 -0
- package/src/config/oidcEnvironment.ts +59 -0
- package/src/config/serverlessFunctionEnv.test.ts +98 -0
- package/src/config/stageConfig.test.ts +26 -0
- package/src/config/stageConfig.ts +24 -0
- package/src/docker-entry.ts +13 -0
- package/src/helpers/auth.helpers.ts +68 -30
- package/src/helpers/escrowCancelLink.test.ts +60 -0
- package/src/helpers/escrowCancelLink.ts +38 -0
- package/src/helpers/escrowCancelToken.test.ts +105 -0
- package/src/helpers/escrowCancelToken.ts +47 -0
- package/src/helpers/keycloak.helpers.ts +120 -0
- package/src/helpers/learnCard.helpers.test.ts +11 -1
- package/src/helpers/learnCard.helpers.ts +57 -45
- package/src/helpers/learnCard.signingAuthority.test.ts +194 -0
- package/src/helpers/login-ticket.helpers.test.ts +45 -0
- package/src/helpers/login-ticket.helpers.ts +91 -0
- package/src/helpers/oidc.helpers.ts +354 -0
- package/src/helpers/rate-limit.helpers.test.ts +61 -0
- package/src/helpers/rate-limit.helpers.ts +73 -0
- package/src/helpers/rateLimit.helpers.ts +112 -0
- package/src/helpers/seedEncryption.helpers.test.ts +427 -0
- package/src/helpers/seedEncryption.helpers.ts +372 -0
- package/src/helpers/signingAuthority.helpers.ts +4 -1
- package/src/helpers/social-token.helpers.ts +144 -0
- package/src/index.ts +1 -0
- package/src/jobs/escrowBlobRewrap.ts +121 -0
- package/src/jobs/escrowHoldReminders.ts +177 -0
- package/src/migrations/signingAuthoritySeeds.ts +430 -0
- package/src/migrations/signingAuthoritySeedsCli.test.ts +70 -0
- package/src/models/AuthSubject.ts +147 -0
- package/src/models/EscrowHold.ts +541 -0
- package/src/models/SigningAuthority.ts +14 -3
- package/src/models/UserKey.escrowPin.test.ts +49 -0
- package/src/models/UserKey.ts +1109 -40
- package/src/models/index.ts +2 -0
- package/src/models/pruneOrphanedRecoveryMethods.ts +3 -1
- package/src/oidc.ts +371 -0
- package/src/routes/auth.ts +173 -0
- package/src/routes/credentials.ts +8 -0
- package/src/routes/escrow.ts +1073 -0
- package/src/routes/firebase.ts +90 -48
- package/src/routes/index.ts +98 -5
- package/src/routes/keys.ts +1022 -174
- package/src/routes/qr-login.ts +51 -53
- package/src/routes/signing-authority.ts +39 -1
- package/src/services/delivery/adapters/postmark.adapter.ts +48 -1
- package/src/services/escrow-enclave/README.md +122 -0
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-ca.pem +11 -0
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.key +5 -0
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.pem +12 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-ca.pem +11 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.key +5 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.pem +12 -0
- package/src/services/escrow-enclave/blobIdentity.test.ts +42 -0
- package/src/services/escrow-enclave/blobIdentity.ts +55 -0
- package/src/services/escrow-enclave/cancellation.ts +59 -0
- package/src/services/escrow-enclave/enclaveTrustAnchors.ts +34 -0
- package/src/services/escrow-enclave/index.ts +174 -0
- package/src/services/escrow-enclave/notifications.test.ts +377 -0
- package/src/services/escrow-enclave/notifications.ts +348 -0
- package/src/services/escrow-enclave/production.test.ts +34 -0
- package/src/services/escrow-enclave/remoteEnclave.contract.test.ts +344 -0
- package/src/services/escrow-enclave/remoteEnclave.test.ts +490 -0
- package/src/services/escrow-enclave/remoteEnclave.tls.test.ts +62 -0
- package/src/services/escrow-enclave/remoteEnclave.ts +263 -0
- package/src/services/escrow-enclave/softwareEnclave.test.ts +450 -0
- package/src/services/escrow-enclave/softwareEnclave.ts +254 -0
- package/src/services/escrow-enclave/types.ts +122 -0
- package/src/types/seed-encryption.ts +51 -0
- package/src/types/seed-migration.ts +61 -0
- package/test/auth-tickets.spec.ts +480 -0
- package/test/credentials.spec.ts +2 -0
- package/test/helpers/keycloak-broker.ts +181 -0
- package/test/helpers/live-mongo.ts +9 -0
- package/test/jobs/escrowBlobRewrap.spec.ts +262 -0
- package/test/jobs/escrowHoldReminders.spec.ts +319 -0
- package/test/keycloak-admin.spec.ts +58 -0
- package/test/keycloak-broker-roundtrip.integration.spec.ts +40 -0
- package/test/keycloak-migration.integration.spec.ts +266 -0
- package/test/keycloak-provider-links.spec.ts +201 -0
- package/test/keycloak-social-broker.integration.spec.ts +228 -0
- package/test/keycloak-verify.integration.spec.ts +100 -0
- package/test/keycloak-verify.spec.ts +307 -0
- package/test/keys-escrow.spec.ts +2328 -0
- package/test/keys-identity-rebind.spec.ts +456 -0
- package/test/keys-lifecycle-invariants.spec.ts +785 -0
- package/test/keys-recovery-confirmation.spec.ts +586 -0
- package/test/keys-recovery-rate-limit.spec.ts +123 -0
- package/test/keys-secret-leakage.spec.ts +265 -0
- package/test/models/authSubjectIndexes.spec.ts +132 -0
- package/test/models/escrow.spec.ts +572 -0
- package/test/notifications.spec.ts +4 -0
- package/test/oidc-e2e/broker.spec.ts +116 -0
- package/test/oidc-e2e/compose.yaml +33 -0
- package/test/oidc-e2e/fixtures.ts +245 -0
- package/test/oidc-e2e/lock.test.mjs +73 -0
- package/test/oidc-e2e/playwright.config.ts +21 -0
- package/test/oidc-e2e/run.mjs +312 -0
- package/test/oidc-e2e/tsconfig.json +10 -0
- package/test/oidc-e2e/types.ts +21 -0
- package/test/oidc.integration.spec.ts +54 -0
- package/test/oidc.spec.ts +697 -0
- package/test/oidcLambdaApp.boot.spec.ts +105 -0
- package/test/provision-keycloak-users.spec.ts +299 -0
- package/test/signing-authority-http.spec.ts +161 -0
- package/test/signing-authority-seed-migration.spec.ts +414 -0
- package/test/signing-authority.spec.ts +90 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +1 -0
- package/vitest.config.ts +24 -1
- package/vitest.integration.config.ts +25 -3
- package/dist/cache/signing-authority.d.ts +0 -6
- package/src/cache/signing-authority.ts +0 -37
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { setTimeout } from 'node:timers/promises';
|
|
2
|
+
import { KeycloakAdminError, type createKeycloakAdmin } from './keycloak-admin';
|
|
3
|
+
|
|
4
|
+
export interface FirebaseProvider {
|
|
5
|
+
providerId: string;
|
|
6
|
+
uid: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FirebaseUser {
|
|
11
|
+
uid: string;
|
|
12
|
+
disabled: boolean;
|
|
13
|
+
emailVerified: boolean;
|
|
14
|
+
email?: string;
|
|
15
|
+
providerData: FirebaseProvider[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FirebaseReader {
|
|
19
|
+
getUsers: (identifiers: { uid: string }[]) => Promise<{ users: FirebaseUser[] }>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProviderLinkSummary {
|
|
23
|
+
linked: number;
|
|
24
|
+
alreadyLinked: number;
|
|
25
|
+
conflicts: number;
|
|
26
|
+
noSocialProvider: number;
|
|
27
|
+
wouldLink: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Firebase accepts at most 100 identifiers per call; keep requests sequential and paced. */
|
|
31
|
+
export const fetchFirebaseUsers = async (
|
|
32
|
+
uids: string[],
|
|
33
|
+
firebase: FirebaseReader,
|
|
34
|
+
sleep: (ms: number) => Promise<void> = setTimeout
|
|
35
|
+
): Promise<Map<string, FirebaseUser>> => {
|
|
36
|
+
const unique = [...new Set(uids)];
|
|
37
|
+
const users = new Map<string, FirebaseUser>();
|
|
38
|
+
for (let offset = 0; offset < unique.length; offset += 100) {
|
|
39
|
+
if (offset) await sleep(1000);
|
|
40
|
+
const identifiers = unique.slice(offset, offset + 100).map(uid => ({ uid }));
|
|
41
|
+
for (let attempt = 0; ; attempt++) {
|
|
42
|
+
try {
|
|
43
|
+
const result = await firebase.getUsers(identifiers);
|
|
44
|
+
for (const user of result.users) users.set(user.uid, user);
|
|
45
|
+
break;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const code =
|
|
48
|
+
typeof error === 'object' && error !== null && 'code' in error
|
|
49
|
+
? error.code
|
|
50
|
+
: undefined;
|
|
51
|
+
if (
|
|
52
|
+
attempt >= 3 ||
|
|
53
|
+
(code !== 'auth/too-many-requests' && code !== 'auth/quota-exceeded')
|
|
54
|
+
) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
await sleep(1000 * 2 ** attempt);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return users;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Read-only preflight: reject the whole user before adding any provider links. */
|
|
65
|
+
export const findProviderLinkConflicts = (
|
|
66
|
+
providers: FirebaseProvider[],
|
|
67
|
+
links: { identityProvider: string; userId: string }[]
|
|
68
|
+
): string[] =>
|
|
69
|
+
['google', 'apple'].filter(alias => {
|
|
70
|
+
const matches = providers.filter(provider => provider.providerId === `${alias}.com`);
|
|
71
|
+
if (!matches.length) return false;
|
|
72
|
+
const existing = links.find(link => link.identityProvider === alias);
|
|
73
|
+
return (
|
|
74
|
+
matches.some(provider => !provider.uid) ||
|
|
75
|
+
new Set(matches.map(provider => provider.uid)).size > 1 ||
|
|
76
|
+
Boolean(existing && existing.userId !== matches[0]!.uid)
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** Only add identities; never remove or reassign a provider link, even on conflicts. */
|
|
81
|
+
export const linkFirebaseProviders = async ({
|
|
82
|
+
userId,
|
|
83
|
+
providers,
|
|
84
|
+
apply,
|
|
85
|
+
admin,
|
|
86
|
+
log,
|
|
87
|
+
summary = { linked: 0, alreadyLinked: 0, conflicts: 0, noSocialProvider: 0, wouldLink: 0 },
|
|
88
|
+
}: {
|
|
89
|
+
userId?: string;
|
|
90
|
+
providers: FirebaseProvider[];
|
|
91
|
+
apply: boolean;
|
|
92
|
+
admin: Pick<Awaited<ReturnType<typeof createKeycloakAdmin>>, 'links' | 'request'>;
|
|
93
|
+
log: (message: string) => void;
|
|
94
|
+
summary?: ProviderLinkSummary;
|
|
95
|
+
}): Promise<ProviderLinkSummary> => {
|
|
96
|
+
const social = providers.filter(
|
|
97
|
+
provider => provider.providerId === 'google.com' || provider.providerId === 'apple.com'
|
|
98
|
+
);
|
|
99
|
+
if (!social.length) {
|
|
100
|
+
summary.noSocialProvider++;
|
|
101
|
+
return summary;
|
|
102
|
+
}
|
|
103
|
+
const links = userId ? await admin.links(userId) : [];
|
|
104
|
+
const conflicts = findProviderLinkConflicts(providers, links);
|
|
105
|
+
if (conflicts.length) {
|
|
106
|
+
summary.conflicts += conflicts.length;
|
|
107
|
+
for (const alias of conflicts)
|
|
108
|
+
log(`CONFLICT: ${alias} identity differs; manual review required`);
|
|
109
|
+
return summary;
|
|
110
|
+
}
|
|
111
|
+
for (const alias of ['google', 'apple']) {
|
|
112
|
+
const matches = social.filter(provider => provider.providerId === `${alias}.com`);
|
|
113
|
+
if (!matches.length) continue;
|
|
114
|
+
const provider = matches[0]!;
|
|
115
|
+
const existing = links.find(link => link.identityProvider === alias);
|
|
116
|
+
if (existing) {
|
|
117
|
+
summary.alreadyLinked++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!apply) {
|
|
121
|
+
summary.wouldLink++;
|
|
122
|
+
log(`PLAN: link ${alias} (ownership conflicts checked on apply)`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!userId) throw new Error('A provisioned Keycloak user is required for linking');
|
|
126
|
+
try {
|
|
127
|
+
await admin.request(
|
|
128
|
+
`/users/${encodeURIComponent(userId)}/federated-identity/${alias}`,
|
|
129
|
+
{
|
|
130
|
+
method: 'POST',
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
identityProvider: alias,
|
|
133
|
+
userId: provider.uid,
|
|
134
|
+
userName: provider.email || provider.uid,
|
|
135
|
+
}),
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
summary.linked++;
|
|
139
|
+
log(`Linked ${alias}`);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (!(error instanceof KeycloakAdminError) || error.status !== 409) throw error;
|
|
142
|
+
summary.conflicts++;
|
|
143
|
+
log(`CONFLICT: ${alias} identity already linked; manual review required`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return summary;
|
|
147
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run one local migration phase using the API container's database and keys.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
usage() {
|
|
6
|
+
echo 'Usage: bun run migrate:sa-seeds:local --dry-run|--prepare|--verify|--purge'
|
|
7
|
+
echo 'Defaults to read-only dry-run. Runs one phase to completion and streams logs.'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if [[ "$#" -gt 1 ]]; then
|
|
11
|
+
usage >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
case "${1:---dry-run}" in
|
|
16
|
+
--dry-run|--prepare|--verify|--purge) SA_PHASE="${1:---dry-run}"; SA_PHASE="${SA_PHASE#--}" ;;
|
|
17
|
+
--help|-h) usage; exit 0 ;;
|
|
18
|
+
*) usage >&2; exit 1 ;;
|
|
19
|
+
esac
|
|
20
|
+
|
|
21
|
+
SA_REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
|
22
|
+
cd "$SA_REPO_ROOT"
|
|
23
|
+
printf 'Running local signing-authority migration: %s\n' "$SA_PHASE"
|
|
24
|
+
|
|
25
|
+
docker compose -f apps/learn-card-app/compose-local.yaml exec -T \
|
|
26
|
+
-e "SA_MIGRATION_PHASE=$SA_PHASE" \
|
|
27
|
+
-w /app/services/learn-card-network/lca-api api bun --conditions=development -e '
|
|
28
|
+
import { environment } from "./src/config/environment";
|
|
29
|
+
import { client, mongodb } from "./src/mongo";
|
|
30
|
+
import { runSeedMigrationBatch, SeedMigrationError } from "./src/migrations/signingAuthoritySeeds";
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
let result;
|
|
34
|
+
do {
|
|
35
|
+
result = await runSeedMigrationBatch(
|
|
36
|
+
mongodb,
|
|
37
|
+
{ phase: process.env.SA_MIGRATION_PHASE, batchSize: 50 },
|
|
38
|
+
{ encryptedWritesEnabled: environment.SA_SEED_ENCRYPT_WRITES }
|
|
39
|
+
);
|
|
40
|
+
console.log(JSON.stringify(result, null, 2));
|
|
41
|
+
if (!result.done && result.processed === 0 && !result.rescanRequired) {
|
|
42
|
+
throw new Error("No progress");
|
|
43
|
+
}
|
|
44
|
+
} while (!result.done);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(error instanceof SeedMigrationError ? error.category : "operation_failed");
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
} finally {
|
|
49
|
+
await client.close();
|
|
50
|
+
}
|
|
51
|
+
'
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
|
|
8
|
+
const { values } = parseArgs({
|
|
9
|
+
options: {
|
|
10
|
+
'function-name': { type: 'string' },
|
|
11
|
+
region: { type: 'string' },
|
|
12
|
+
phase: { type: 'string', default: 'dry-run' },
|
|
13
|
+
'batch-size': { type: 'string', default: '50' },
|
|
14
|
+
'one-batch': { type: 'boolean', default: false },
|
|
15
|
+
help: { type: 'boolean', default: false },
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (values.help) {
|
|
20
|
+
console.log(
|
|
21
|
+
'Usage: node scripts/migrate-signing-authority-seeds.mjs --function-name NAME --region REGION --phase dry-run|prepare|verify|purge [--batch-size 1..100] [--one-batch]'
|
|
22
|
+
);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
const batchSize = Number(values['batch-size']);
|
|
26
|
+
if (
|
|
27
|
+
!values['function-name'] ||
|
|
28
|
+
!values.region ||
|
|
29
|
+
!['dry-run', 'prepare', 'verify', 'purge'].includes(values.phase) ||
|
|
30
|
+
!Number.isInteger(batchSize) ||
|
|
31
|
+
batchSize < 1 ||
|
|
32
|
+
batchSize > 100
|
|
33
|
+
) {
|
|
34
|
+
console.error('Provide --function-name, --region, a valid --phase, and --batch-size 1..100.');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// No database credentials or key material on the operator host. AWS CLI uses its normal credential chain.
|
|
39
|
+
const directory = mkdtempSync(join(tmpdir(), 'lca-sa-migration-'));
|
|
40
|
+
const payloadFile = join(directory, 'request.json');
|
|
41
|
+
const outputFile = join(directory, 'response.json');
|
|
42
|
+
writeFileSync(payloadFile, JSON.stringify({ phase: values.phase, batchSize }), { mode: 0o600 });
|
|
43
|
+
try {
|
|
44
|
+
let done = false;
|
|
45
|
+
do {
|
|
46
|
+
const invocation = JSON.parse(
|
|
47
|
+
execFileSync(
|
|
48
|
+
'aws',
|
|
49
|
+
[
|
|
50
|
+
'lambda',
|
|
51
|
+
'invoke',
|
|
52
|
+
'--function-name',
|
|
53
|
+
values['function-name'],
|
|
54
|
+
'--region',
|
|
55
|
+
values.region,
|
|
56
|
+
'--invocation-type',
|
|
57
|
+
'RequestResponse',
|
|
58
|
+
'--payload',
|
|
59
|
+
`fileb://${payloadFile}`,
|
|
60
|
+
'--cli-read-timeout',
|
|
61
|
+
'910',
|
|
62
|
+
'--cli-connect-timeout',
|
|
63
|
+
'10',
|
|
64
|
+
'--output',
|
|
65
|
+
'json',
|
|
66
|
+
outputFile,
|
|
67
|
+
],
|
|
68
|
+
{
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
71
|
+
env: { ...process.env, AWS_PAGER: '' },
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
const result = JSON.parse(readFileSync(outputFile, 'utf8'));
|
|
76
|
+
if (invocation.FunctionError || invocation.StatusCode !== 200) {
|
|
77
|
+
// The Lambda boundary emits only sanitized migration errors.
|
|
78
|
+
console.error(
|
|
79
|
+
result.errorMessage ?? 'Migration invocation failed. Inspect the function logs.'
|
|
80
|
+
);
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
console.log(JSON.stringify(result));
|
|
85
|
+
done = result.done === true;
|
|
86
|
+
if (!done && result.processed === 0 && !result.rescanRequired) {
|
|
87
|
+
console.error('No progress in this batch. Inspect the function logs before retrying.');
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
} while (!done && !values['one-batch']);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// CLI diagnostics describe invocation/auth/transport failures. Never print the whole
|
|
94
|
+
// exec error: stdout and the Lambda response are handled separately above.
|
|
95
|
+
if (typeof error.stderr === 'string' || Buffer.isBuffer(error.stderr)) {
|
|
96
|
+
const diagnostic = error.stderr.toString().trim();
|
|
97
|
+
if (diagnostic) console.error(diagnostic);
|
|
98
|
+
}
|
|
99
|
+
console.error(
|
|
100
|
+
'AWS invocation failed. Check your credentials, region, and function name; rerun to resume.'
|
|
101
|
+
);
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
} finally {
|
|
104
|
+
rmSync(directory, { recursive: true, force: true });
|
|
105
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Prints the signing-authority seed rollout flags a deployment loads from its checked-in
|
|
2
|
+
// config.<tenant>.<stage>.json. Usage: CONFIG_TENANT=scouts bun run sa-seed:flags production
|
|
3
|
+
import { resolveStageDefaults } from '@learncard/service-config';
|
|
4
|
+
|
|
5
|
+
import { base, stages } from '../src/config/stageConfig';
|
|
6
|
+
|
|
7
|
+
const stage = process.argv[2];
|
|
8
|
+
if (!stage)
|
|
9
|
+
throw new Error(
|
|
10
|
+
'Usage: bun run sa-seed:flags <dev|production> (CONFIG_TENANT selects the tenant)'
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const resolved = resolveStageDefaults({ base, stages, stage, env: process.env });
|
|
14
|
+
console.log(
|
|
15
|
+
JSON.stringify({
|
|
16
|
+
tenant: process.env.CONFIG_TENANT || 'learncard',
|
|
17
|
+
stage,
|
|
18
|
+
SA_SEED_ENCRYPT_WRITES: resolved.SA_SEED_ENCRYPT_WRITES ?? '(schema default)',
|
|
19
|
+
SA_SEED_ALLOW_LEGACY_READ: resolved.SA_SEED_ALLOW_LEGACY_READ ?? '(schema default)',
|
|
20
|
+
})
|
|
21
|
+
);
|