@choiceapps/communityservices-sdk 0.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/AGENTS.md +43 -0
- package/API.md +86 -0
- package/CHANGELOG.md +12 -0
- package/LICENSE +201 -0
- package/NOTICE +11 -0
- package/README.md +100 -0
- package/SSVF.md +131 -0
- package/browser/attachment-conflict.mjs +6 -0
- package/browser/case-plans-ui.mjs +23 -0
- package/browser/case-plans.d.mts +20 -0
- package/browser/case-plans.mjs +89 -0
- package/browser/client-creation.d.mts +16 -0
- package/browser/client-creation.mjs +60 -0
- package/browser/client-profile-ui.mjs +59 -0
- package/browser/client-profile.d.mts +24 -0
- package/browser/client-profile.mjs +94 -0
- package/browser/client-workflows.mjs +135 -0
- package/browser/connection.d.mts +4 -0
- package/browser/connection.mjs +41 -0
- package/browser/current-living-situation.d.mts +44 -0
- package/browser/current-living-situation.mjs +161 -0
- package/browser/date-of-birth-ui.mjs +68 -0
- package/browser/date-of-birth.d.mts +28 -0
- package/browser/date-of-birth.mjs +112 -0
- package/browser/enrollment-workflow.d.mts +174 -0
- package/browser/enrollment-workflow.mjs +518 -0
- package/browser/exit-details.mjs +21 -0
- package/browser/file-store.mjs +32 -0
- package/browser/gwt-mapped.mjs +46 -0
- package/browser/household-exit.d.mts +34 -0
- package/browser/household-exit.mjs +102 -0
- package/browser/households-ui.mjs +20 -0
- package/browser/households.d.mts +22 -0
- package/browser/households.mjs +115 -0
- package/browser/index.d.mts +78 -0
- package/browser/index.mjs +46 -0
- package/browser/member-entry-date.d.mts +30 -0
- package/browser/member-entry-date.mjs +71 -0
- package/browser/need-recording.d.mts +37 -0
- package/browser/need-recording.mjs +68 -0
- package/browser/need-ui.mjs +138 -0
- package/browser/page-lock.mjs +11 -0
- package/browser/password-recovery.mjs +49 -0
- package/browser/playwright-session.mjs +196 -0
- package/browser/program-discovery.d.mts +22 -0
- package/browser/program-discovery.mjs +80 -0
- package/browser/recovery-diagnostics.mjs +9 -0
- package/browser/service-recording.d.mts +45 -0
- package/browser/service-recording.mjs +148 -0
- package/browser/service-ui.mjs +160 -0
- package/browser/session-authentication.mjs +40 -0
- package/browser/shelter-inventory.d.mts +103 -0
- package/browser/shelter-inventory.mjs +273 -0
- package/browser/shelter-ui.mjs +382 -0
- package/browser/shelters.d.mts +139 -0
- package/browser/shelters.mjs +425 -0
- package/browser/ssvf-rules.d.mts +21 -0
- package/browser/ssvf-rules.mjs +145 -0
- package/browser/ui-login.mjs +135 -0
- package/dist/cjs/bulk.d.ts +33 -0
- package/dist/cjs/bulk.d.ts.map +1 -0
- package/dist/cjs/bulk.js +42 -0
- package/dist/cjs/bulk.js.map +1 -0
- package/dist/cjs/control.d.ts +35 -0
- package/dist/cjs/control.d.ts.map +1 -0
- package/dist/cjs/control.js +141 -0
- package/dist/cjs/control.js.map +1 -0
- package/dist/cjs/effective-date.d.ts +3 -0
- package/dist/cjs/effective-date.d.ts.map +1 -0
- package/dist/cjs/effective-date.js +27 -0
- package/dist/cjs/effective-date.js.map +1 -0
- package/dist/cjs/encoding.d.ts +32 -0
- package/dist/cjs/encoding.d.ts.map +1 -0
- package/dist/cjs/encoding.js +121 -0
- package/dist/cjs/encoding.js.map +1 -0
- package/dist/cjs/enrollment-verifications.d.ts +39 -0
- package/dist/cjs/enrollment-verifications.d.ts.map +1 -0
- package/dist/cjs/enrollment-verifications.js +96 -0
- package/dist/cjs/enrollment-verifications.js.map +1 -0
- package/dist/cjs/errors.d.ts +33 -0
- package/dist/cjs/errors.d.ts.map +1 -0
- package/dist/cjs/errors.js +45 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/generated/v2-models.d.ts +1482 -0
- package/dist/cjs/generated/v2-models.d.ts.map +1 -0
- package/dist/cjs/generated/v2-models.js +3 -0
- package/dist/cjs/generated/v2-models.js.map +1 -0
- package/dist/cjs/generated/v2.d.ts +1149 -0
- package/dist/cjs/generated/v2.d.ts.map +1 -0
- package/dist/cjs/generated/v2.js +252 -0
- package/dist/cjs/generated/v2.js.map +1 -0
- package/dist/cjs/generated/v3-models.d.ts +1420 -0
- package/dist/cjs/generated/v3-models.d.ts.map +1 -0
- package/dist/cjs/generated/v3-models.js +3 -0
- package/dist/cjs/generated/v3-models.js.map +1 -0
- package/dist/cjs/generated/v3.d.ts +1149 -0
- package/dist/cjs/generated/v3.d.ts.map +1 -0
- package/dist/cjs/generated/v3.js +252 -0
- package/dist/cjs/generated/v3.js.map +1 -0
- package/dist/cjs/gwt-enrollments.d.ts +131 -0
- package/dist/cjs/gwt-enrollments.d.ts.map +1 -0
- package/dist/cjs/gwt-enrollments.js +280 -0
- package/dist/cjs/gwt-enrollments.js.map +1 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +19 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/metadata.d.ts +100 -0
- package/dist/cjs/metadata.d.ts.map +1 -0
- package/dist/cjs/metadata.js +165 -0
- package/dist/cjs/metadata.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/program-discovery.d.ts +28 -0
- package/dist/cjs/program-discovery.d.ts.map +1 -0
- package/dist/cjs/program-discovery.js +58 -0
- package/dist/cjs/program-discovery.js.map +1 -0
- package/dist/cjs/race.d.ts +4 -0
- package/dist/cjs/race.d.ts.map +1 -0
- package/dist/cjs/race.js +40 -0
- package/dist/cjs/race.js.map +1 -0
- package/dist/cjs/runtime.d.ts +108 -0
- package/dist/cjs/runtime.d.ts.map +1 -0
- package/dist/cjs/runtime.js +38 -0
- package/dist/cjs/runtime.js.map +1 -0
- package/dist/cjs/subassessments.d.ts +29 -0
- package/dist/cjs/subassessments.d.ts.map +1 -0
- package/dist/cjs/subassessments.js +107 -0
- package/dist/cjs/subassessments.js.map +1 -0
- package/dist/cjs/testing.d.ts +45 -0
- package/dist/cjs/testing.d.ts.map +1 -0
- package/dist/cjs/testing.js +66 -0
- package/dist/cjs/testing.js.map +1 -0
- package/dist/cjs/transport.d.ts +19 -0
- package/dist/cjs/transport.d.ts.map +1 -0
- package/dist/cjs/transport.js +373 -0
- package/dist/cjs/transport.js.map +1 -0
- package/dist/cjs/v2.d.ts +13 -0
- package/dist/cjs/v2.d.ts.map +1 -0
- package/dist/cjs/v2.js +29 -0
- package/dist/cjs/v2.js.map +1 -0
- package/dist/cjs/v3.d.ts +13 -0
- package/dist/cjs/v3.d.ts.map +1 -0
- package/dist/cjs/v3.js +29 -0
- package/dist/cjs/v3.js.map +1 -0
- package/dist/cjs/validation.d.ts +33 -0
- package/dist/cjs/validation.d.ts.map +1 -0
- package/dist/cjs/validation.js +77 -0
- package/dist/cjs/validation.js.map +1 -0
- package/dist/cjs/verification.d.ts +82 -0
- package/dist/cjs/verification.d.ts.map +1 -0
- package/dist/cjs/verification.js +122 -0
- package/dist/cjs/verification.js.map +1 -0
- package/dist/esm/bulk.d.ts +33 -0
- package/dist/esm/bulk.d.ts.map +1 -0
- package/dist/esm/bulk.js +39 -0
- package/dist/esm/bulk.js.map +1 -0
- package/dist/esm/control.d.ts +35 -0
- package/dist/esm/control.d.ts.map +1 -0
- package/dist/esm/control.js +132 -0
- package/dist/esm/control.js.map +1 -0
- package/dist/esm/effective-date.d.ts +3 -0
- package/dist/esm/effective-date.d.ts.map +1 -0
- package/dist/esm/effective-date.js +24 -0
- package/dist/esm/effective-date.js.map +1 -0
- package/dist/esm/encoding.d.ts +32 -0
- package/dist/esm/encoding.d.ts.map +1 -0
- package/dist/esm/encoding.js +117 -0
- package/dist/esm/encoding.js.map +1 -0
- package/dist/esm/enrollment-verifications.d.ts +39 -0
- package/dist/esm/enrollment-verifications.d.ts.map +1 -0
- package/dist/esm/enrollment-verifications.js +93 -0
- package/dist/esm/enrollment-verifications.js.map +1 -0
- package/dist/esm/errors.d.ts +33 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +33 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/generated/v2-models.d.ts +1482 -0
- package/dist/esm/generated/v2-models.d.ts.map +1 -0
- package/dist/esm/generated/v2-models.js +2 -0
- package/dist/esm/generated/v2-models.js.map +1 -0
- package/dist/esm/generated/v2.d.ts +1149 -0
- package/dist/esm/generated/v2.d.ts.map +1 -0
- package/dist/esm/generated/v2.js +247 -0
- package/dist/esm/generated/v2.js.map +1 -0
- package/dist/esm/generated/v3-models.d.ts +1420 -0
- package/dist/esm/generated/v3-models.d.ts.map +1 -0
- package/dist/esm/generated/v3-models.js +2 -0
- package/dist/esm/generated/v3-models.js.map +1 -0
- package/dist/esm/generated/v3.d.ts +1149 -0
- package/dist/esm/generated/v3.d.ts.map +1 -0
- package/dist/esm/generated/v3.js +247 -0
- package/dist/esm/generated/v3.js.map +1 -0
- package/dist/esm/gwt-enrollments.d.ts +131 -0
- package/dist/esm/gwt-enrollments.d.ts.map +1 -0
- package/dist/esm/gwt-enrollments.js +274 -0
- package/dist/esm/gwt-enrollments.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/metadata.d.ts +100 -0
- package/dist/esm/metadata.d.ts.map +1 -0
- package/dist/esm/metadata.js +157 -0
- package/dist/esm/metadata.js.map +1 -0
- package/dist/esm/program-discovery.d.ts +28 -0
- package/dist/esm/program-discovery.d.ts.map +1 -0
- package/dist/esm/program-discovery.js +55 -0
- package/dist/esm/program-discovery.js.map +1 -0
- package/dist/esm/race.d.ts +4 -0
- package/dist/esm/race.d.ts.map +1 -0
- package/dist/esm/race.js +37 -0
- package/dist/esm/race.js.map +1 -0
- package/dist/esm/runtime.d.ts +108 -0
- package/dist/esm/runtime.d.ts.map +1 -0
- package/dist/esm/runtime.js +33 -0
- package/dist/esm/runtime.js.map +1 -0
- package/dist/esm/subassessments.d.ts +29 -0
- package/dist/esm/subassessments.d.ts.map +1 -0
- package/dist/esm/subassessments.js +103 -0
- package/dist/esm/subassessments.js.map +1 -0
- package/dist/esm/testing.d.ts +45 -0
- package/dist/esm/testing.d.ts.map +1 -0
- package/dist/esm/testing.js +60 -0
- package/dist/esm/testing.js.map +1 -0
- package/dist/esm/transport.d.ts +19 -0
- package/dist/esm/transport.d.ts.map +1 -0
- package/dist/esm/transport.js +369 -0
- package/dist/esm/transport.js.map +1 -0
- package/dist/esm/v2.d.ts +13 -0
- package/dist/esm/v2.d.ts.map +1 -0
- package/dist/esm/v2.js +13 -0
- package/dist/esm/v2.js.map +1 -0
- package/dist/esm/v3.d.ts +13 -0
- package/dist/esm/v3.d.ts.map +1 -0
- package/dist/esm/v3.js +13 -0
- package/dist/esm/v3.js.map +1 -0
- package/dist/esm/validation.d.ts +33 -0
- package/dist/esm/validation.d.ts.map +1 -0
- package/dist/esm/validation.js +72 -0
- package/dist/esm/validation.js.map +1 -0
- package/dist/esm/verification.d.ts +82 -0
- package/dist/esm/verification.d.ts.map +1 -0
- package/dist/esm/verification.js +117 -0
- package/dist/esm/verification.js.map +1 -0
- package/docs/README.md +56 -0
- package/docs/assessments.md +103 -0
- package/docs/browser-session.md +129 -0
- package/docs/capabilities.md +75 -0
- package/docs/configuration.md +81 -0
- package/docs/current-living-situation.md +56 -0
- package/docs/dates-and-answers.md +40 -0
- package/docs/development.md +81 -0
- package/docs/durable-records.md +156 -0
- package/docs/enrollments.md +217 -0
- package/docs/files-and-notes.md +39 -0
- package/docs/getting-started.md +56 -0
- package/docs/independent-needs.md +77 -0
- package/docs/integration-methods.md +60 -0
- package/docs/profiles.md +68 -0
- package/docs/reference.md +64 -0
- package/docs/rest-client.md +97 -0
- package/docs/services.md +66 -0
- package/docs/shelter-inventory.md +64 -0
- package/docs/shelters.md +75 -0
- package/docs/troubleshooting.md +61 -0
- package/docs/using-the-sdk.md +203 -0
- package/docs/vendor-issues/README.md +46 -0
- package/docs/vendor-issues/api-coverage.md +27 -0
- package/docs/vendor-issues/attachment-authentication.md +40 -0
- package/docs/vendor-issues/date-semantics.md +51 -0
- package/docs/vendor-issues/empty-service-list-404.md +51 -0
- package/docs/vendor-issues/enrollment-complete-target-error.md +178 -0
- package/docs/vendor-issues/enrollment-effective-date-column.md +112 -0
- package/docs/vendor-issues/household-head-validation.md +26 -0
- package/docs/vendor-issues/write-contracts.md +51 -0
- package/examples/README.md +41 -0
- package/examples/current-living-situation.mts +27 -0
- package/examples/durable-records.mts +78 -0
- package/examples/enrollment-workflow.mts +46 -0
- package/examples/gwt-enrollment-session.ts +17 -0
- package/examples/gwt-file-store.mjs +2 -0
- package/examples/household-enrollment-workflow.mts +43 -0
- package/examples/household-exit-workflow.mts +35 -0
- package/examples/household-lifecycle-workflow.mts +34 -0
- package/examples/independent-needs.mts +30 -0
- package/examples/program-discovery.ts +19 -0
- package/examples/quickstart.mjs +13 -0
- package/examples/shelter-inventory.mts +30 -0
- package/examples/shelters.mts +33 -0
- package/examples/usage.ts +23 -0
- package/examples/verification-snapshot.ts +9 -0
- package/llms.txt +26 -0
- package/package.json +103 -0
- package/specs/provenance.json +17 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {resolveUiConnection} from './connection.mjs';
|
|
2
|
+
import {readFile} from 'node:fs/promises';
|
|
3
|
+
import {parseEnv} from 'node:util';
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
import {homedir} from 'node:os';
|
|
6
|
+
import {join} from 'node:path';
|
|
7
|
+
import {createFileGwtMutationStore} from './file-store.mjs';
|
|
8
|
+
import {attachAuthentication} from './session-authentication.mjs';
|
|
9
|
+
import {withPageLock} from './page-lock.mjs';
|
|
10
|
+
import {waitForLogin,submitExpiredPassword,restoreOriginalPassword,probeAuthentication} from './password-recovery.mjs';
|
|
11
|
+
|
|
12
|
+
export class LoginError extends Error {
|
|
13
|
+
constructor(code,stage,recoveryStage){
|
|
14
|
+
const detail=code==='PASSWORD_EXPIRED'?'Password expired; passwordRecovery is disabled':code==='RECOVERY_UNCERTAIN'?'Password recovery requires reconciliation; no uncertain password change was replayed':code==='RECOVERY_CONFIGURATION'?'Invalid password recovery configuration or journal':'Headless authentication failed; no automatic credential retry was performed';
|
|
15
|
+
super(`Login: ${detail} (stage: ${stage}${recoveryStage?', recovery: '+recoveryStage:''})`);
|
|
16
|
+
this.name='LoginError';this.code=code;this.stage=stage;this.recoveryStage=recoveryStage;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const stages=new Set(['temporary-attempted','temporary-active','temporary-verification-attempted','restore-attempted','restored','original-verification-attempted','original-verified']);
|
|
20
|
+
|
|
21
|
+
/** Fresh headless application context. Recovery defaults on and never retries an uncertain password write.
|
|
22
|
+
* Credentials stay in memory; the original .env is unchanged. Share the recovery store across callers. */
|
|
23
|
+
export async function openUiSession(options) {
|
|
24
|
+
const connection=resolveUiConnection(options);
|
|
25
|
+
options={...options,...connection};
|
|
26
|
+
const session=await loginUiSession(options);
|
|
27
|
+
const auth=attachAuthentication(session.page,async()=>{
|
|
28
|
+
const refreshed=await loginUiSession(options,session);
|
|
29
|
+
session.passwordRecovery=refreshed.passwordRecovery;
|
|
30
|
+
},()=>probeAuthentication(session.context,connection.moduleBaseUrl,Math.min(options.timeoutMs??90000,15000)),connection);
|
|
31
|
+
const close=session.close;
|
|
32
|
+
session.ensureAuthenticated=()=>withPageLock(session.page,async()=>{});
|
|
33
|
+
session.reauthenticate=()=>withPageLock(session.page,()=>auth.reauthenticate(),{authentication:false});
|
|
34
|
+
session.close=()=>withPageLock(session.page,async()=>{auth.close();await close();},{authentication:false});
|
|
35
|
+
return session;
|
|
36
|
+
}
|
|
37
|
+
async function loginUiSession({chromium,tenant,moduleBaseUrl,envFile='.env',executablePath,timeoutMs=90000,passwordRecovery},existing) {
|
|
38
|
+
const connection=resolveUiConnection({tenant,moduleBaseUrl}),target=new URL(connection.moduleBaseUrl);
|
|
39
|
+
let browser=existing?.browser,stage='credentials',record,usedExisting=false;
|
|
40
|
+
const extraContexts=[];
|
|
41
|
+
const failure=code=>new LoginError(code,stage,record?.stage);
|
|
42
|
+
try {
|
|
43
|
+
const env=parseEnv(await readFile(envFile,'utf8'));
|
|
44
|
+
if(!env.HMIS_UI_USER||!env.HMIS_UI_PASSWORD)throw failure('LOGIN_FAILED');
|
|
45
|
+
const recovery=passwordRecovery!==false,appendCharacter=passwordRecovery?.appendCharacter??'!';
|
|
46
|
+
if(passwordRecovery!==undefined&&passwordRecovery!==false&&(!passwordRecovery||typeof passwordRecovery!=='object'||Array.isArray(passwordRecovery)))throw failure('RECOVERY_CONFIGURATION');
|
|
47
|
+
const store=passwordRecovery?.store??createFileGwtMutationStore(env.HMIS_UI_PASSWORD_RECOVERY_DIR||join(homedir(),'.wellsky-sdk','password-recovery'));
|
|
48
|
+
if(recovery&&(!store||!['withLock','read','write'].every(k=>typeof store[k]==='function')||typeof appendCharacter!=='string'||!/^[!-~]$/.test(appendCharacter)))throw failure('RECOVERY_CONFIGURATION');
|
|
49
|
+
const accountHash=createHash('sha256').update(env.HMIS_UI_USER.trim().toLowerCase()).digest('hex');
|
|
50
|
+
const scope=JSON.stringify([connection.scope,'password-recovery',accountHash]);
|
|
51
|
+
const run=async()=>{
|
|
52
|
+
let records=[];
|
|
53
|
+
if(recovery){
|
|
54
|
+
records=await store.read(scope);
|
|
55
|
+
if(!Array.isArray(records)||records.some((r,i)=>!r||r.version!==1||!stages.has(r.stage)||typeof r.appendCharacter!=='string'||!/^[!-~]$/.test(r.appendCharacter)||i<records.length-1&&r.stage!=='original-verified'))throw failure('RECOVERY_CONFIGURATION');
|
|
56
|
+
record=records.at(-1);
|
|
57
|
+
if(record?.stage==='original-verified')record=undefined;
|
|
58
|
+
if(record&&(env.HMIS_UI_PASSWORD.length<8||env.HMIS_UI_PASSWORD.length>=50))throw failure('RECOVERY_CONFIGURATION');
|
|
59
|
+
if(record&&record.appendCharacter!==appendCharacter)throw failure('RECOVERY_CONFIGURATION');
|
|
60
|
+
if(record&&['temporary-verification-attempted','original-verification-attempted'].includes(record.stage))throw failure('RECOVERY_UNCERTAIN');
|
|
61
|
+
}
|
|
62
|
+
const persist=async value=>{record.stage=value;await store.write(scope,structuredClone(records));};
|
|
63
|
+
stage='launch';browser??=await chromium.launch({headless:true,chromiumSandbox:true,...(executablePath?{executablePath}:{})});
|
|
64
|
+
const login=async(password,fresh=false)=>{
|
|
65
|
+
const reuse=existing&&!usedExisting&&!fresh;
|
|
66
|
+
if(reuse){usedExisting=true;await existing.context.clearCookies();}
|
|
67
|
+
const context=reuse?existing.context:await browser.newContext({viewport:{width:1280,height:900}});
|
|
68
|
+
if(existing&&!reuse)extraContexts.push(context);
|
|
69
|
+
if(!reuse)await context.route('**/*',async route=>{
|
|
70
|
+
const request=route.request(),url=new URL(request.url());
|
|
71
|
+
if((request.isNavigationRequest()||!['GET','HEAD','OPTIONS'].includes(request.method()))&&(url.origin!==target.origin||!url.pathname.startsWith('/'+tenant+'/')))return route.abort();
|
|
72
|
+
return route.continue();
|
|
73
|
+
});
|
|
74
|
+
const page=reuse?existing.page:await context.newPage();page.setDefaultTimeout(timeoutMs);
|
|
75
|
+
stage='open-application';await page.goto(connection.moduleBaseUrl+'index.html',{waitUntil:'domcontentloaded',timeout:timeoutMs});
|
|
76
|
+
await page.locator('[id="formfield-login"]').fill(env.HMIS_UI_USER);
|
|
77
|
+
await page.locator('[id="formfield-password"]').fill(password);
|
|
78
|
+
stage='login';await page.locator('[id="LoginView.fbtn_submit"]').click();
|
|
79
|
+
const outcome=await waitForLogin(page,timeoutMs);
|
|
80
|
+
return {context,page,outcome};
|
|
81
|
+
};
|
|
82
|
+
const finish=async(session,recovered)=>{
|
|
83
|
+
if(existing&&session.page!==existing.page){
|
|
84
|
+
// Transfer only this configured origin's verified cookies in memory. Keep the Page identity
|
|
85
|
+
// used by existing adapters, including servers that replace the earlier login session.
|
|
86
|
+
const cookies=await session.context.cookies(connection.moduleBaseUrl);
|
|
87
|
+
await existing.context.clearCookies();await existing.context.addCookies(cookies);
|
|
88
|
+
await session.context.close();
|
|
89
|
+
await existing.page.goto(connection.moduleBaseUrl+'index.html',{waitUntil:'domcontentloaded',timeout:timeoutMs});
|
|
90
|
+
if(await waitForLogin(existing.page,timeoutMs)!=='dashboard')throw failure('RECOVERY_UNCERTAIN');
|
|
91
|
+
session=existing;
|
|
92
|
+
}
|
|
93
|
+
return {connection,browser,context:session.context,page:session.page,passwordRecovery:recovered?'restored-original':'not-needed',close:()=>browser.close()};
|
|
94
|
+
};
|
|
95
|
+
const temporary=env.HMIS_UI_PASSWORD+appendCharacter;
|
|
96
|
+
// A lost save acknowledgement permits one authentication probe, never another save.
|
|
97
|
+
if(record&&['restore-attempted','restored'].includes(record.stage)){
|
|
98
|
+
await persist('original-verification-attempted');
|
|
99
|
+
const session=await login(env.HMIS_UI_PASSWORD);
|
|
100
|
+
if(session.outcome!=='dashboard')throw failure('RECOVERY_UNCERTAIN');
|
|
101
|
+
await persist('original-verified');return finish(session,true);
|
|
102
|
+
}
|
|
103
|
+
const resuming=!!record;
|
|
104
|
+
if(resuming)await persist('temporary-verification-attempted');
|
|
105
|
+
let session=await login(resuming?temporary:env.HMIS_UI_PASSWORD);
|
|
106
|
+
if(resuming){
|
|
107
|
+
if(session.outcome!=='dashboard')throw failure('RECOVERY_UNCERTAIN');
|
|
108
|
+
await persist('temporary-active');
|
|
109
|
+
}else if(session.outcome==='dashboard')return finish(session,false);
|
|
110
|
+
else{
|
|
111
|
+
if(!recovery)throw failure('PASSWORD_EXPIRED');
|
|
112
|
+
if(env.HMIS_UI_PASSWORD.length<8||env.HMIS_UI_PASSWORD.length>=50)throw failure('RECOVERY_CONFIGURATION');
|
|
113
|
+
record={version:1,appendCharacter,stage:'temporary-attempted'};records.push(record);
|
|
114
|
+
stage='temporary-password';
|
|
115
|
+
await submitExpiredPassword(session.page,temporary,()=>persist('temporary-attempted'),timeoutMs);
|
|
116
|
+
await persist('temporary-active');
|
|
117
|
+
}
|
|
118
|
+
stage='restore-original';
|
|
119
|
+
await restoreOriginalPassword(session.page,temporary,env.HMIS_UI_PASSWORD,()=>persist('restore-attempted'),timeoutMs);
|
|
120
|
+
await persist('restored');
|
|
121
|
+
if(!existing)await session.context.close();
|
|
122
|
+
await persist('original-verification-attempted');
|
|
123
|
+
session=await login(env.HMIS_UI_PASSWORD,true);
|
|
124
|
+
if(session.outcome!=='dashboard')throw failure('RECOVERY_UNCERTAIN');
|
|
125
|
+
await persist('original-verified');return finish(session,true);
|
|
126
|
+
};
|
|
127
|
+
return recovery?await store.withLock(scope,run):await run();
|
|
128
|
+
}catch(error){
|
|
129
|
+
if(!existing)await browser?.close().catch(()=>{});
|
|
130
|
+
else await Promise.allSettled(extraContexts.map(context=>context.close()));
|
|
131
|
+
if(error instanceof LoginError)throw error;
|
|
132
|
+
// Playwright errors can include fill arguments. Never propagate their message or cause.
|
|
133
|
+
throw failure(record&&record.stage!=='original-verified'?'RECOVERY_UNCERTAIN':'LOGIN_FAILED');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { WellSkyError } from "./runtime.js";
|
|
2
|
+
export type BulkItemResult<T> = {
|
|
3
|
+
index: number;
|
|
4
|
+
outcome: "fulfilled";
|
|
5
|
+
value: T;
|
|
6
|
+
} | {
|
|
7
|
+
index: number;
|
|
8
|
+
outcome: "rejected";
|
|
9
|
+
error: WellSkyError;
|
|
10
|
+
} | {
|
|
11
|
+
index: number;
|
|
12
|
+
outcome: "skipped";
|
|
13
|
+
reason: "cancelled" | "callback_failed";
|
|
14
|
+
};
|
|
15
|
+
export interface BulkReport<T> {
|
|
16
|
+
results: BulkItemResult<T>[];
|
|
17
|
+
callbackErrors: {
|
|
18
|
+
index: number;
|
|
19
|
+
error: WellSkyError;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
export interface BulkOptions<T> {
|
|
23
|
+
concurrency?: number;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
/** Called as each item completes. Awaited before scheduling the next item on that worker. */
|
|
26
|
+
onResult?: (result: BulkItemResult<T>) => void | Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/** No item is retried here. A rejected write promise does not prove the write was rolled back. */
|
|
29
|
+
export declare function mapBulk<I, O>(items: readonly I[], worker: (item: I, context: {
|
|
30
|
+
index: number;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}) => Promise<O>, options?: BulkOptions<O>): Promise<BulkReport<O>>;
|
|
33
|
+
//# sourceMappingURL=bulk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulk.d.ts","sourceRoot":"","sources":["../../src/bulk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,MAAM,MAAM,cAAc,CAAC,CAAC,IACxB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACjD;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,GAC3D;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,iBAAiB,CAAA;CAAE,CAAC;AACnF,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7B,cAAc,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,YAAY,CAAA;KAAE,EAAE,CAAC;CAC1D;AACD,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChE;AAED,kGAAkG;AAClG,wBAAsB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,WAAW,CAAC,CAAC,CAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CA0BhM"}
|
package/dist/cjs/bulk.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapBulk = mapBulk;
|
|
4
|
+
const runtime_js_1 = require("./runtime.js");
|
|
5
|
+
const errors_js_1 = require("./errors.js");
|
|
6
|
+
/** No item is retried here. A rejected write promise does not prove the write was rolled back. */
|
|
7
|
+
async function mapBulk(items, worker, options = {}) {
|
|
8
|
+
const concurrency = options.concurrency ?? 8;
|
|
9
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1)
|
|
10
|
+
throw new errors_js_1.ValidationError("bulk configuration", ["concurrency"]);
|
|
11
|
+
const results = [];
|
|
12
|
+
const callbackErrors = [];
|
|
13
|
+
let next = 0;
|
|
14
|
+
let callbackFailed = false;
|
|
15
|
+
const jobs = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
16
|
+
while (next < items.length && !options.signal?.aborted && !callbackFailed) {
|
|
17
|
+
const index = next++;
|
|
18
|
+
// The index is reserved synchronously and is bounded by items.length.
|
|
19
|
+
const item = items[index];
|
|
20
|
+
let result;
|
|
21
|
+
try {
|
|
22
|
+
result = { index, outcome: "fulfilled", value: await worker(item, { index, ...(options.signal ? { signal: options.signal } : {}) }) };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
result = { index, outcome: "rejected", error: error instanceof runtime_js_1.WellSkyError ? error : new runtime_js_1.WellSkyError("protocol", "bulk item") };
|
|
26
|
+
}
|
|
27
|
+
results[index] = result;
|
|
28
|
+
try {
|
|
29
|
+
await options.onResult?.(result);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
callbackFailed = true;
|
|
33
|
+
callbackErrors.push({ index, error: new runtime_js_1.WellSkyError("protocol", "bulk result callback") });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
await Promise.all(jobs);
|
|
38
|
+
for (let index = next; index < items.length; index++)
|
|
39
|
+
results[index] = { index, outcome: "skipped", reason: callbackFailed ? "callback_failed" : "cancelled" };
|
|
40
|
+
return { results, callbackErrors };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=bulk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulk.js","sourceRoot":"","sources":["../../src/bulk.ts"],"names":[],"mappings":";;AAmBA,0BA0BC;AA7CD,6CAA4C;AAC5C,2CAA8C;AAiB9C,kGAAkG;AAC3F,KAAK,UAAU,OAAO,CAAO,KAAmB,EAAE,MAAiF,EAAE,UAA0B,EAAE;IACtK,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC;IAC7C,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC;QAAE,MAAM,IAAI,2BAAe,CAAC,oBAAoB,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;IAC5H,MAAM,OAAO,GAAwB,EAAE,CAAC;IACxC,MAAM,cAAc,GAAoC,EAAE,CAAC;IAC3D,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,cAAc,GAAG,KAAK,CAAC;IAC3B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,IAAI,EAAE;QAClF,OAAO,IAAI,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC,cAAc,EAAE,CAAC;YAC1E,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC;YACrB,sEAAsE;YACtE,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAM,CAAC;YAC/B,IAAI,MAAyB,CAAC;YAC9B,IAAI,CAAC;gBAAC,MAAM,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;YAAC,CAAC;YAC9I,OAAO,KAAK,EAAE,CAAC;gBAAC,MAAM,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,YAAY,yBAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,yBAAY,CAAC,UAAU,EAAE,WAAW,CAAC,EAAE,CAAC;YAAC,CAAC;YACpJ,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC;YACxB,IAAI,CAAC;gBAAC,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC;YAAC,CAAC;YACzC,MAAM,CAAC;gBACL,cAAc,GAAG,IAAI,CAAC;gBACtB,cAAc,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,yBAAY,CAAC,UAAU,EAAE,sBAAsB,CAAC,EAAE,CAAC,CAAC;YAC9F,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACxB,KAAK,IAAI,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE;QAAE,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAC/J,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC;AACrC,CAAC","sourcesContent":["import { WellSkyError } from \"./runtime.js\";\nimport { ValidationError } from \"./errors.js\";\n\nexport type BulkItemResult<T> =\n | { index: number; outcome: \"fulfilled\"; value: T }\n | { index: number; outcome: \"rejected\"; error: WellSkyError }\n | { index: number; outcome: \"skipped\"; reason: \"cancelled\" | \"callback_failed\" };\nexport interface BulkReport<T> {\n results: BulkItemResult<T>[];\n callbackErrors: { index: number; error: WellSkyError }[];\n}\nexport interface BulkOptions<T> {\n concurrency?: number;\n signal?: AbortSignal;\n /** Called as each item completes. Awaited before scheduling the next item on that worker. */\n onResult?: (result: BulkItemResult<T>) => void | Promise<void>;\n}\n\n/** No item is retried here. A rejected write promise does not prove the write was rolled back. */\nexport async function mapBulk<I, O>(items: readonly I[], worker: (item: I, context: { index: number; signal?: AbortSignal }) => Promise<O>, options: BulkOptions<O> = {}): Promise<BulkReport<O>> {\n const concurrency = options.concurrency ?? 8;\n if (!Number.isSafeInteger(concurrency) || concurrency < 1) throw new ValidationError(\"bulk configuration\", [\"concurrency\"]);\n const results: BulkItemResult<O>[] = [];\n const callbackErrors: BulkReport<O>[\"callbackErrors\"] = [];\n let next = 0;\n let callbackFailed = false;\n const jobs = Array.from({ length: Math.min(concurrency, items.length) }, async () => {\n while (next < items.length && !options.signal?.aborted && !callbackFailed) {\n const index = next++;\n // The index is reserved synchronously and is bounded by items.length.\n const item = items[index] as I;\n let result: BulkItemResult<O>;\n try { result = { index, outcome: \"fulfilled\", value: await worker(item, { index, ...(options.signal ? { signal: options.signal } : {}) }) }; }\n catch (error) { result = { index, outcome: \"rejected\", error: error instanceof WellSkyError ? error : new WellSkyError(\"protocol\", \"bulk item\") }; }\n results[index] = result;\n try { await options.onResult?.(result); }\n catch {\n callbackFailed = true;\n callbackErrors.push({ index, error: new WellSkyError(\"protocol\", \"bulk result callback\") });\n }\n }\n });\n await Promise.all(jobs);\n for (let index = next; index < items.length; index++) results[index] = { index, outcome: \"skipped\", reason: callbackFailed ? \"callback_failed\" : \"cancelled\" };\n return { results, callbackErrors };\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ClientEvent, ClientHealth, ClientOptions } from "./runtime.js";
|
|
2
|
+
export declare class Semaphore {
|
|
3
|
+
#private;
|
|
4
|
+
active: number;
|
|
5
|
+
constructor(capacity: number);
|
|
6
|
+
get queued(): number;
|
|
7
|
+
acquire(signal: AbortSignal, operation: string): Promise<() => void>;
|
|
8
|
+
}
|
|
9
|
+
export interface SharedAuthentication {
|
|
10
|
+
cached?: {
|
|
11
|
+
token: string;
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
} | undefined;
|
|
14
|
+
inFlight?: Promise<string> | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare function identity(value: object): number;
|
|
17
|
+
/** Credentials are hashed for cache identity and never exposed in keys or diagnostics. */
|
|
18
|
+
export declare function sharedAuthentication(scope: string, auth: ClientOptions["auth"]): Promise<SharedAuthentication>;
|
|
19
|
+
export declare const tokenMintQueue: Semaphore;
|
|
20
|
+
export declare class RequestControl {
|
|
21
|
+
#private;
|
|
22
|
+
readonly semaphore: Semaphore;
|
|
23
|
+
tokenMintCount: number;
|
|
24
|
+
constructor(concurrency: number, settings: {
|
|
25
|
+
threshold: number;
|
|
26
|
+
windowMs: number;
|
|
27
|
+
durationMs: number;
|
|
28
|
+
});
|
|
29
|
+
check(operation: string): void;
|
|
30
|
+
failed(status: number): number | undefined;
|
|
31
|
+
health(): ClientHealth;
|
|
32
|
+
}
|
|
33
|
+
export declare function requestControl(scope: string, options: ClientOptions): RequestControl;
|
|
34
|
+
export declare function emit(options: Pick<ClientOptions, "hooks" | "logger">, event: ClientEvent): void;
|
|
35
|
+
//# sourceMappingURL=control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../src/control.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAI7E,qBAAa,SAAS;;IACpB,MAAM,SAAK;gBAGC,QAAQ,EAAE,MAAM;IAC5B,IAAI,MAAM,IAAI,MAAM,CAA+B;IACnD,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC;CAuBrE;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAC1D,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;CACxC;AAGD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI9C;AAED,0FAA0F;AAC1F,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,oBAAoB,CAAC,CASpH;AACD,eAAO,MAAM,cAAc,WAAmB,CAAC;AAE/C,qBAAa,cAAc;;IACzB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAI9B,cAAc,SAAK;gBACP,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE;IAItG,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI9B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAQ1C,MAAM,IAAI,YAAY;CAGvB;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,cAAc,CASpF;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,QAAQ,CAAC,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAO/F"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RequestControl = exports.tokenMintQueue = exports.Semaphore = void 0;
|
|
4
|
+
exports.identity = identity;
|
|
5
|
+
exports.sharedAuthentication = sharedAuthentication;
|
|
6
|
+
exports.requestControl = requestControl;
|
|
7
|
+
exports.emit = emit;
|
|
8
|
+
const runtime_js_1 = require("./runtime.js");
|
|
9
|
+
const errors_js_1 = require("./errors.js");
|
|
10
|
+
class Semaphore {
|
|
11
|
+
active = 0;
|
|
12
|
+
#capacity;
|
|
13
|
+
#queue = [];
|
|
14
|
+
constructor(capacity) { this.#capacity = capacity; }
|
|
15
|
+
get queued() { return this.#queue.length; }
|
|
16
|
+
acquire(signal, operation) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
let released = false;
|
|
19
|
+
const grant = () => {
|
|
20
|
+
signal.removeEventListener("abort", abort);
|
|
21
|
+
this.active++;
|
|
22
|
+
resolve(() => {
|
|
23
|
+
if (released)
|
|
24
|
+
return;
|
|
25
|
+
released = true;
|
|
26
|
+
this.active--;
|
|
27
|
+
this.#queue.shift()?.grant();
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
const abort = () => {
|
|
31
|
+
const index = this.#queue.indexOf(waiter);
|
|
32
|
+
if (index >= 0)
|
|
33
|
+
this.#queue.splice(index, 1);
|
|
34
|
+
reject(new runtime_js_1.WellSkyError(signal.reason instanceof DOMException && signal.reason.name === "TimeoutError" ? "timeout" : "aborted", operation));
|
|
35
|
+
};
|
|
36
|
+
const waiter = { grant, signal, abort };
|
|
37
|
+
if (signal.aborted) {
|
|
38
|
+
abort();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (this.active < this.#capacity)
|
|
42
|
+
grant();
|
|
43
|
+
else {
|
|
44
|
+
this.#queue.push(waiter);
|
|
45
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.Semaphore = Semaphore;
|
|
51
|
+
const identities = new WeakMap();
|
|
52
|
+
let nextIdentity = 1;
|
|
53
|
+
function identity(value) {
|
|
54
|
+
const prior = identities.get(value);
|
|
55
|
+
if (prior !== undefined)
|
|
56
|
+
return prior;
|
|
57
|
+
const id = nextIdentity++;
|
|
58
|
+
identities.set(value, id);
|
|
59
|
+
return id;
|
|
60
|
+
}
|
|
61
|
+
const authentication = new Map();
|
|
62
|
+
/** Credentials are hashed for cache identity and never exposed in keys or diagnostics. */
|
|
63
|
+
async function sharedAuthentication(scope, auth) {
|
|
64
|
+
const descriptor = auth.kind === "credentials" ? [scope, auth.kind, auth.username, auth.password] : auth.kind === "provider" ? [scope, auth.kind, identity(auth.getToken)] : [scope, auth.kind];
|
|
65
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(descriptor)));
|
|
66
|
+
const key = Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
67
|
+
const prior = authentication.get(key);
|
|
68
|
+
if (prior)
|
|
69
|
+
return prior;
|
|
70
|
+
const state = {};
|
|
71
|
+
authentication.set(key, state);
|
|
72
|
+
return state;
|
|
73
|
+
}
|
|
74
|
+
exports.tokenMintQueue = new Semaphore(1);
|
|
75
|
+
class RequestControl {
|
|
76
|
+
semaphore;
|
|
77
|
+
#settings;
|
|
78
|
+
#failures = [];
|
|
79
|
+
#until = 0;
|
|
80
|
+
tokenMintCount = 0;
|
|
81
|
+
constructor(concurrency, settings) {
|
|
82
|
+
for (const value of [concurrency, settings.threshold, settings.windowMs, settings.durationMs])
|
|
83
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
84
|
+
throw new TypeError("Concurrency and cooldown settings must be positive integers");
|
|
85
|
+
this.semaphore = new Semaphore(concurrency);
|
|
86
|
+
this.#settings = { ...settings };
|
|
87
|
+
}
|
|
88
|
+
check(operation) {
|
|
89
|
+
if (this.#until > Date.now())
|
|
90
|
+
throw new errors_js_1.CooldownError(operation, this.#until);
|
|
91
|
+
if (this.#until !== 0) {
|
|
92
|
+
this.#until = 0;
|
|
93
|
+
this.#failures = [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
failed(status) {
|
|
97
|
+
if (status !== 401 && status !== 403 && status !== 429 && status < 500)
|
|
98
|
+
return undefined;
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
this.#failures = this.#failures.filter(time => time > now - this.#settings.windowMs);
|
|
101
|
+
this.#failures.push(now);
|
|
102
|
+
if (this.#failures.length >= this.#settings.threshold && this.#until <= now) {
|
|
103
|
+
this.#until = now + this.#settings.durationMs;
|
|
104
|
+
return this.#until;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
health() {
|
|
109
|
+
return { state: this.#until > Date.now() ? "open" : "closed", until: this.#until > Date.now() ? new Date(this.#until).toISOString() : null, activeRequests: this.semaphore.active, queuedRequests: this.semaphore.queued, tokenMintCount: this.tokenMintCount };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.RequestControl = RequestControl;
|
|
113
|
+
const controls = new Map();
|
|
114
|
+
function requestControl(scope, options) {
|
|
115
|
+
const concurrency = options.maxConcurrency ?? 8;
|
|
116
|
+
const cooldown = options.cooldown ?? { threshold: 5, windowMs: 10_000, durationMs: 30_000 };
|
|
117
|
+
const signature = JSON.stringify([concurrency, cooldown.threshold, cooldown.windowMs, cooldown.durationMs]);
|
|
118
|
+
const existing = controls.get(scope);
|
|
119
|
+
if (existing && existing.signature !== signature)
|
|
120
|
+
throw new TypeError("Clients sharing a tenant and transport must use the same concurrency/cooldown settings");
|
|
121
|
+
if (existing)
|
|
122
|
+
return existing.value;
|
|
123
|
+
const value = new RequestControl(concurrency, cooldown);
|
|
124
|
+
controls.set(scope, { signature, value });
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
function emit(options, event) {
|
|
128
|
+
const hooks = options.hooks;
|
|
129
|
+
const handler = event.type === "request" ? hooks?.onRequest : event.type === "response" ? hooks?.onResponse : event.type === "tokenMint" ? hooks?.onTokenMint : event.type === "throttle" ? hooks?.onThrottle : hooks?.onCooldown;
|
|
130
|
+
const safe = Object.freeze({ ...event });
|
|
131
|
+
// Observers must not turn a completed write into a thrown error or a replay.
|
|
132
|
+
try {
|
|
133
|
+
void Promise.resolve(handler?.(safe)).catch(() => { });
|
|
134
|
+
}
|
|
135
|
+
catch { /* Observer failures cannot change request outcomes. */ }
|
|
136
|
+
try {
|
|
137
|
+
void Promise.resolve(options.logger?.(safe)).catch(() => { });
|
|
138
|
+
}
|
|
139
|
+
catch { /* Same boundary for user-supplied loggers. */ }
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control.js","sourceRoot":"","sources":["../../src/control.ts"],"names":[],"mappings":"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","sourcesContent":["import { WellSkyError } from \"./runtime.js\";\nimport type { ClientEvent, ClientHealth, ClientOptions } from \"./runtime.js\";\nimport { CooldownError } from \"./errors.js\";\n\ninterface Waiter { grant: () => void; signal: AbortSignal; abort: () => void }\nexport class Semaphore {\n active = 0;\n readonly #capacity: number;\n readonly #queue: Waiter[] = [];\n constructor(capacity: number) { this.#capacity = capacity; }\n get queued(): number { return this.#queue.length; }\n acquire(signal: AbortSignal, operation: string): Promise<() => void> {\n return new Promise((resolve, reject) => {\n let released = false;\n const grant = () => {\n signal.removeEventListener(\"abort\", abort);\n this.active++;\n resolve(() => {\n if (released) return;\n released = true; this.active--;\n this.#queue.shift()?.grant();\n });\n };\n const abort = () => {\n const index = this.#queue.indexOf(waiter);\n if (index >= 0) this.#queue.splice(index, 1);\n reject(new WellSkyError(signal.reason instanceof DOMException && signal.reason.name === \"TimeoutError\" ? \"timeout\" : \"aborted\", operation));\n };\n const waiter: Waiter = { grant, signal, abort };\n if (signal.aborted) { abort(); return; }\n if (this.active < this.#capacity) grant();\n else { this.#queue.push(waiter); signal.addEventListener(\"abort\", abort, { once: true }); }\n });\n }\n}\n\nexport interface SharedAuthentication {\n cached?: { token: string; expiresAt: number } | undefined;\n inFlight?: Promise<string> | undefined;\n}\nconst identities = new WeakMap<object, number>();\nlet nextIdentity = 1;\nexport function identity(value: object): number {\n const prior = identities.get(value);\n if (prior !== undefined) return prior;\n const id = nextIdentity++; identities.set(value, id); return id;\n}\nconst authentication = new Map<string, SharedAuthentication>();\n/** Credentials are hashed for cache identity and never exposed in keys or diagnostics. */\nexport async function sharedAuthentication(scope: string, auth: ClientOptions[\"auth\"]): Promise<SharedAuthentication> {\n const descriptor = auth.kind === \"credentials\" ? [scope, auth.kind, auth.username, auth.password] : auth.kind === \"provider\" ? [scope, auth.kind, identity(auth.getToken)] : [scope, auth.kind];\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(JSON.stringify(descriptor)));\n const key = Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, \"0\")).join(\"\");\n const prior = authentication.get(key);\n if (prior) return prior;\n const state: SharedAuthentication = {};\n authentication.set(key, state);\n return state;\n}\nexport const tokenMintQueue = new Semaphore(1);\n\nexport class RequestControl {\n readonly semaphore: Semaphore;\n readonly #settings: { threshold: number; windowMs: number; durationMs: number };\n #failures: number[] = [];\n #until = 0;\n tokenMintCount = 0;\n constructor(concurrency: number, settings: { threshold: number; windowMs: number; durationMs: number }) {\n for (const value of [concurrency, settings.threshold, settings.windowMs, settings.durationMs]) if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(\"Concurrency and cooldown settings must be positive integers\");\n this.semaphore = new Semaphore(concurrency); this.#settings = { ...settings };\n }\n check(operation: string): void {\n if (this.#until > Date.now()) throw new CooldownError(operation, this.#until);\n if (this.#until !== 0) { this.#until = 0; this.#failures = []; }\n }\n failed(status: number): number | undefined {\n if (status !== 401 && status !== 403 && status !== 429 && status < 500) return undefined;\n const now = Date.now();\n this.#failures = this.#failures.filter(time => time > now - this.#settings.windowMs);\n this.#failures.push(now);\n if (this.#failures.length >= this.#settings.threshold && this.#until <= now) { this.#until = now + this.#settings.durationMs; return this.#until; }\n return undefined;\n }\n health(): ClientHealth {\n return { state: this.#until > Date.now() ? \"open\" : \"closed\", until: this.#until > Date.now() ? new Date(this.#until).toISOString() : null, activeRequests: this.semaphore.active, queuedRequests: this.semaphore.queued, tokenMintCount: this.tokenMintCount };\n }\n}\nconst controls = new Map<string, { signature: string; value: RequestControl }>();\nexport function requestControl(scope: string, options: ClientOptions): RequestControl {\n const concurrency = options.maxConcurrency ?? 8;\n const cooldown = options.cooldown ?? { threshold: 5, windowMs: 10_000, durationMs: 30_000 };\n const signature = JSON.stringify([concurrency, cooldown.threshold, cooldown.windowMs, cooldown.durationMs]);\n const existing = controls.get(scope);\n if (existing && existing.signature !== signature) throw new TypeError(\"Clients sharing a tenant and transport must use the same concurrency/cooldown settings\");\n if (existing) return existing.value;\n const value = new RequestControl(concurrency, cooldown);\n controls.set(scope, { signature, value }); return value;\n}\n\nexport function emit(options: Pick<ClientOptions, \"hooks\" | \"logger\">, event: ClientEvent): void {\n const hooks = options.hooks;\n const handler = event.type === \"request\" ? hooks?.onRequest : event.type === \"response\" ? hooks?.onResponse : event.type === \"tokenMint\" ? hooks?.onTokenMint : event.type === \"throttle\" ? hooks?.onThrottle : hooks?.onCooldown;\n const safe = Object.freeze({ ...event });\n // Observers must not turn a completed write into a thrown error or a replay.\n try { void Promise.resolve(handler?.(safe)).catch(() => {}); } catch { /* Observer failures cannot change request outcomes. */ }\n try { void Promise.resolve(options.logger?.(safe)).catch(() => {}); } catch { /* Same boundary for user-supplied loggers. */ }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effective-date.d.ts","sourceRoot":"","sources":["../../src/effective-date.ts"],"names":[],"mappings":"AAEA,uFAAuF;AACvF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAcrG"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.correctMobileEffectiveDate = correctMobileEffectiveDate;
|
|
4
|
+
const errors_js_1 = require("./errors.js");
|
|
5
|
+
/** Explicit deployment correction for an observed upstream mobile-write date shift. */
|
|
6
|
+
function correctMobileEffectiveDate(value, minutes, operation) {
|
|
7
|
+
const invalid = () => new errors_js_1.ValidationError(operation, ["dateEffective"]);
|
|
8
|
+
if (typeof value !== "string")
|
|
9
|
+
throw invalid();
|
|
10
|
+
const parts = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
11
|
+
if (!parts)
|
|
12
|
+
throw invalid();
|
|
13
|
+
const [, year, month, day, hour, minute, second, , zone] = parts;
|
|
14
|
+
const date = new Date(`${year}-${month}-${day}T00:00:00.000Z`);
|
|
15
|
+
if (!Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== `${year}-${month}-${day}` || Number(hour) > 23 || Number(minute) > 59 || Number(second) > 59)
|
|
16
|
+
throw invalid();
|
|
17
|
+
if (zone !== "Z" && (!zone || Number(zone.slice(1, 3)) > 14 || Number(zone.slice(4)) > 59 || (Number(zone.slice(1, 3)) === 14 && Number(zone.slice(4)) !== 0)))
|
|
18
|
+
throw invalid();
|
|
19
|
+
const corrected = Date.parse(value) + minutes * 60_000;
|
|
20
|
+
if (!Number.isFinite(corrected))
|
|
21
|
+
throw invalid();
|
|
22
|
+
const encoded = new Date(corrected).toISOString();
|
|
23
|
+
if (!/^\d{4}-/.test(encoded))
|
|
24
|
+
throw invalid();
|
|
25
|
+
return encoded;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=effective-date.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effective-date.js","sourceRoot":"","sources":["../../src/effective-date.ts"],"names":[],"mappings":";;AAGA,gEAcC;AAjBD,2CAA8C;AAE9C,uFAAuF;AACvF,SAAgB,0BAA0B,CAAC,KAAc,EAAE,OAAe,EAAE,SAAiB;IAC3F,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,IAAI,2BAAe,CAAC,SAAS,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IACxE,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,OAAO,EAAE,CAAC;IAC/C,MAAM,KAAK,GAAG,sFAAsF,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjH,IAAI,CAAC,KAAK;QAAE,MAAM,OAAO,EAAE,CAAC;IAC5B,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,AAAD,EAAG,IAAI,CAAC,GAAG,KAAK,CAAC;IACjE,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,IAAI,GAAG,gBAAgB,CAAC,CAAC;IAC/D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI,GAAG,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE;QAAE,MAAM,OAAO,EAAE,CAAC;IAC1L,IAAI,IAAI,KAAK,GAAG,IAAI,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QAAE,MAAM,OAAO,EAAE,CAAC;IAChL,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,OAAO,GAAG,MAAM,CAAC;IACvD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,MAAM,OAAO,EAAE,CAAC;IACjD,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC;IAClD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,MAAM,OAAO,EAAE,CAAC;IAC9C,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["import { ValidationError } from \"./errors.js\";\n\n/** Explicit deployment correction for an observed upstream mobile-write date shift. */\nexport function correctMobileEffectiveDate(value: unknown, minutes: number, operation: string): string {\n const invalid = () => new ValidationError(operation, [\"dateEffective\"]);\n if (typeof value !== \"string\") throw invalid();\n const parts = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,3}))?(Z|[+-]\\d{2}:\\d{2})$/.exec(value);\n if (!parts) throw invalid();\n const [, year, month, day, hour, minute, second, , zone] = parts;\n const date = new Date(`${year}-${month}-${day}T00:00:00.000Z`);\n if (!Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== `${year}-${month}-${day}` || Number(hour) > 23 || Number(minute) > 59 || Number(second) > 59) throw invalid();\n if (zone !== \"Z\" && (!zone || Number(zone.slice(1, 3)) > 14 || Number(zone.slice(4)) > 59 || (Number(zone.slice(1, 3)) === 14 && Number(zone.slice(4)) !== 0))) throw invalid();\n const corrected = Date.parse(value) + minutes * 60_000;\n if (!Number.isFinite(corrected)) throw invalid();\n const encoded = new Date(corrected).toISOString();\n if (!/^\\d{4}-/.test(encoded)) throw invalid();\n return encoded;\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PicklistOption, PicklistSelector, QuestionDefinition } from "./metadata.js";
|
|
2
|
+
export type AnswerState = "client_doesnt_know" | "client_refused" | "data_not_collected";
|
|
3
|
+
export type AnswerValue = {
|
|
4
|
+
value: string | number | boolean | string[];
|
|
5
|
+
} | {
|
|
6
|
+
state: AnswerState;
|
|
7
|
+
} | {
|
|
8
|
+
option: PicklistSelector;
|
|
9
|
+
} | {
|
|
10
|
+
options: readonly PicklistSelector[];
|
|
11
|
+
};
|
|
12
|
+
export type Answers = Record<string, AnswerValue>;
|
|
13
|
+
export interface EncodingOptions {
|
|
14
|
+
endpoint: "standalone" | "enrollment" | "mobile";
|
|
15
|
+
/** Tenant-verified display labels, including exact whitespace/HUD suffixes, for label-based endpoints. */
|
|
16
|
+
writeLabel?: (question: QuestionDefinition, option: PicklistOption) => string | undefined;
|
|
17
|
+
/** Application configuration supplies the relevant option codes. No answer-state labels are guessed. */
|
|
18
|
+
stateCodes?: Partial<Record<AnswerState, readonly string[]>>;
|
|
19
|
+
multiValue?: (question: QuestionDefinition, values: readonly string[]) => string;
|
|
20
|
+
}
|
|
21
|
+
export interface EncodedQuestion {
|
|
22
|
+
question_id: number;
|
|
23
|
+
question_type: string;
|
|
24
|
+
virt_field_name: string;
|
|
25
|
+
answer_value: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function encodeAnswers(questions: readonly QuestionDefinition[], answers: Answers, options: EncodingOptions): EncodedQuestion[];
|
|
28
|
+
/** Builds one new row only. Never accepts or echoes an existing recordset ID. */
|
|
29
|
+
export declare function encodeNewSubassessmentRow(parent: QuestionDefinition, answers: Answers, options: EncodingOptions): {
|
|
30
|
+
subAssessments: EncodedQuestion[];
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=encoding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoding.d.ts","sourceRoot":"","sources":["../../src/encoding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAK1F,MAAM,MAAM,WAAW,GAAG,oBAAoB,GAAG,gBAAgB,GAAG,oBAAoB,CAAC;AACzF,MAAM,MAAM,WAAW,GAAG;IAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,WAAW,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,gBAAgB,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAA;CAAE,CAAC;AAC7K,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAClD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,YAAY,GAAG,YAAY,GAAG,QAAQ,CAAC;IACjD,0GAA0G;IAC1G,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,kBAAkB,EAAE,MAAM,EAAE,cAAc,KAAK,MAAM,GAAG,SAAS,CAAC;IAC1F,wGAAwG;IACxG,UAAU,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IAC7D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,kBAAkB,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;CAClF;AACD,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB;AA2DD,wBAAgB,aAAa,CAAC,SAAS,EAAE,SAAS,kBAAkB,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,GAAG,eAAe,EAAE,CAgBrI;AAED,iFAAiF;AACjF,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,GAAG;IAAE,cAAc,EAAE,eAAe,EAAE,CAAA;CAAE,CAUvJ"}
|