@serfab/cadre-core 0.0.2

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.
Files changed (54) hide show
  1. package/README.md +252 -0
  2. package/dist/arachnode-stub.d.ts +60 -0
  3. package/dist/arachnode-stub.js +120 -0
  4. package/dist/arachnode-stub.js.map +1 -0
  5. package/dist/cadre-node.d.ts +252 -0
  6. package/dist/cadre-node.js +753 -0
  7. package/dist/cadre-node.js.map +1 -0
  8. package/dist/control-database.d.ts +62 -0
  9. package/dist/control-database.js +295 -0
  10. package/dist/control-database.js.map +1 -0
  11. package/dist/enrollment.d.ts +76 -0
  12. package/dist/enrollment.js +126 -0
  13. package/dist/enrollment.js.map +1 -0
  14. package/dist/hibernation-manager.d.ts +68 -0
  15. package/dist/hibernation-manager.js +212 -0
  16. package/dist/hibernation-manager.js.map +1 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.js +24 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/seed-bootstrap.d.ts +164 -0
  21. package/dist/seed-bootstrap.js +554 -0
  22. package/dist/seed-bootstrap.js.map +1 -0
  23. package/dist/strand-database.d.ts +39 -0
  24. package/dist/strand-database.js +108 -0
  25. package/dist/strand-database.js.map +1 -0
  26. package/dist/strand-formation-manager.d.ts +87 -0
  27. package/dist/strand-formation-manager.js +168 -0
  28. package/dist/strand-formation-manager.js.map +1 -0
  29. package/dist/strand-instance-manager.d.ts +60 -0
  30. package/dist/strand-instance-manager.js +209 -0
  31. package/dist/strand-instance-manager.js.map +1 -0
  32. package/dist/strand-solicitation.d.ts +145 -0
  33. package/dist/strand-solicitation.js +180 -0
  34. package/dist/strand-solicitation.js.map +1 -0
  35. package/dist/strand-watcher.d.ts +70 -0
  36. package/dist/strand-watcher.js +136 -0
  37. package/dist/strand-watcher.js.map +1 -0
  38. package/dist/types.d.ts +484 -0
  39. package/dist/types.js +26 -0
  40. package/dist/types.js.map +1 -0
  41. package/package.json +67 -0
  42. package/src/arachnode-stub.ts +160 -0
  43. package/src/cadre-node.ts +923 -0
  44. package/src/control-database.ts +376 -0
  45. package/src/enrollment.ts +199 -0
  46. package/src/hibernation-manager.ts +268 -0
  47. package/src/index.ts +71 -0
  48. package/src/seed-bootstrap.ts +753 -0
  49. package/src/strand-database.ts +168 -0
  50. package/src/strand-formation-manager.ts +277 -0
  51. package/src/strand-instance-manager.ts +281 -0
  52. package/src/strand-solicitation.ts +318 -0
  53. package/src/strand-watcher.ts +197 -0
  54. package/src/types.ts +553 -0
@@ -0,0 +1,145 @@
1
+ import type { Libp2p } from '@libp2p/interface';
2
+ import type { OpenInvitation, FormStrandResult, ValidateFormationResult, StrandFormationDisclosure } from './types.js';
3
+ import { type StrandFormationManagerConfig } from './strand-formation-manager.js';
4
+ /**
5
+ * Interface for validating formation disclosures
6
+ */
7
+ export interface DisclosureValidator {
8
+ /**
9
+ * Validate the disclosure provided by an initiator
10
+ * @param token The invitation token
11
+ * @param disclosure The disclosure object from the initiator
12
+ * @returns Whether the disclosure is acceptable
13
+ */
14
+ validateDisclosure(token: string, disclosure: StrandFormationDisclosure): Promise<boolean>;
15
+ }
16
+ /**
17
+ * Interface for recording formation usage
18
+ */
19
+ export interface FormationUsageRecorder {
20
+ /**
21
+ * Record that a formation invite was used
22
+ */
23
+ recordUsage(token: string, initiatorKey: string, strandId: string): Promise<void>;
24
+ /**
25
+ * Check if a token has already been used (for single-use invites)
26
+ */
27
+ isTokenUsed(token: string): Promise<boolean>;
28
+ /**
29
+ * Check if a token is valid and not expired
30
+ */
31
+ isTokenValid(token: string): Promise<{
32
+ valid: boolean;
33
+ invitation?: OpenInvitation;
34
+ }>;
35
+ }
36
+ /**
37
+ * Interface for strand provisioning during formation
38
+ */
39
+ export interface StrandProvisioner {
40
+ /**
41
+ * Provision a new strand after formation is validated
42
+ */
43
+ provisionStrand(sAppId: string, initiatorKey: string, responderKey: string): Promise<{
44
+ strandId: string;
45
+ }>;
46
+ }
47
+ /**
48
+ * Interface for signing formation approvals
49
+ */
50
+ export interface FormationSigner {
51
+ /**
52
+ * Sign a formation approval
53
+ */
54
+ signFormation(token: string, disclosure: StrandFormationDisclosure): Promise<{
55
+ validationKey: string;
56
+ validationSignature: string;
57
+ }>;
58
+ }
59
+ export interface StrandSolicitationServiceOptions {
60
+ disclosureValidator?: DisclosureValidator;
61
+ formationUsageRecorder?: FormationUsageRecorder;
62
+ strandProvisioner?: StrandProvisioner;
63
+ formationSigner?: FormationSigner;
64
+ /** Party ID for this node (used in protocol messages) */
65
+ partyId?: string;
66
+ /** Cadre peer addresses for this node */
67
+ cadrePeerAddrs?: string[];
68
+ /** Configuration for the formation manager */
69
+ formationConfig?: StrandFormationManagerConfig;
70
+ }
71
+ /**
72
+ * Strand Solicitation API for forming strands via open invitations.
73
+ *
74
+ * This service handles the high-level API defined in api.md:
75
+ * - formStrand(invitation, disclosure, node) - called by initiator
76
+ * - validateStrandFormation(token, disclosure) - called by responder
77
+ *
78
+ * When a libp2p node is provided, the underlying protocol is handled by
79
+ * strand-proto's SessionManager via StrandFormationManager.
80
+ */
81
+ export declare class StrandSolicitationService {
82
+ private readonly disclosureValidator?;
83
+ private readonly formationUsageRecorder?;
84
+ private readonly strandProvisioner?;
85
+ private readonly formationSigner?;
86
+ private readonly partyId;
87
+ private readonly cadrePeerAddrs;
88
+ private formationManager?;
89
+ private readonly formationConfig?;
90
+ constructor(options?: StrandSolicitationServiceOptions);
91
+ /**
92
+ * Get or create the StrandFormationManager.
93
+ * Lazily initialized to allow configuration after construction.
94
+ */
95
+ private getFormationManager;
96
+ /**
97
+ * Register as a responder on a libp2p node.
98
+ * This enables the node to handle incoming strand formation requests.
99
+ */
100
+ registerResponder(node: Libp2p): void;
101
+ /**
102
+ * Unregister as a responder from a libp2p node.
103
+ */
104
+ unregisterResponder(node: Libp2p): void;
105
+ /**
106
+ * Form a strand with a responder via an open invitation.
107
+ *
108
+ * Called by the initiator (the party who received an out-of-band invitation).
109
+ * This generates a member key, contacts the responder's cadre, and negotiates
110
+ * strand formation.
111
+ *
112
+ * @param invitation The open invitation (or just token for legacy API)
113
+ * @param disclosure Identity/context information to share with the responder
114
+ * @param node Optional libp2p node for real protocol handling
115
+ * @returns The member key and strand info if successful
116
+ */
117
+ formStrand(invitation: OpenInvitation | string, disclosure: StrandFormationDisclosure, node?: Libp2p): Promise<FormStrandResult>;
118
+ /**
119
+ * Validate a strand formation request.
120
+ *
121
+ * Called by the responder (the party who created the open invitation) when
122
+ * an initiator contacts them to form a strand. Validates the disclosure
123
+ * and returns a signed approval.
124
+ *
125
+ * @param token The invitation token being used
126
+ * @param disclosure The disclosure from the initiator
127
+ * @returns Validation key and signature if approved
128
+ * @throws If validation fails or token is invalid
129
+ */
130
+ validateStrandFormation(token: string, disclosure: StrandFormationDisclosure): Promise<ValidateFormationResult>;
131
+ /**
132
+ * Create an open invitation for others to form strands with this party.
133
+ *
134
+ * @param sAppId The sApp to use for formed strands
135
+ * @param expirationMs How long the invitation is valid (ms from now)
136
+ * @param bootstrap Bootstrap addresses for contacting this party's cadre
137
+ * @returns The open invitation to share out-of-band
138
+ */
139
+ createOpenInvitation(sAppId: string, expirationMs: number, bootstrap: string[]): Promise<OpenInvitation>;
140
+ /**
141
+ * Record that a formation was completed successfully.
142
+ * Called after strand provisioning to track usage.
143
+ */
144
+ recordFormationComplete(token: string, initiatorKey: string, strandId: string): Promise<void>;
145
+ }
@@ -0,0 +1,180 @@
1
+ import debug from 'debug';
2
+ import { toString as uint8ArrayToString } from 'uint8arrays';
3
+ import { generateKeyPair, privateKeyToProtobuf } from '@libp2p/crypto/keys';
4
+ import { peerIdFromPrivateKey } from '@libp2p/peer-id';
5
+ import { StrandFormationManager } from './strand-formation-manager.js';
6
+ const log = debug('sereus:cadre:solicitation');
7
+ /**
8
+ * Strand Solicitation API for forming strands via open invitations.
9
+ *
10
+ * This service handles the high-level API defined in api.md:
11
+ * - formStrand(invitation, disclosure, node) - called by initiator
12
+ * - validateStrandFormation(token, disclosure) - called by responder
13
+ *
14
+ * When a libp2p node is provided, the underlying protocol is handled by
15
+ * strand-proto's SessionManager via StrandFormationManager.
16
+ */
17
+ export class StrandSolicitationService {
18
+ constructor(options) {
19
+ this.disclosureValidator = options?.disclosureValidator;
20
+ this.formationUsageRecorder = options?.formationUsageRecorder;
21
+ this.strandProvisioner = options?.strandProvisioner;
22
+ this.formationSigner = options?.formationSigner;
23
+ this.partyId = options?.partyId ?? `party-${Date.now()}`;
24
+ this.cadrePeerAddrs = options?.cadrePeerAddrs ?? [];
25
+ this.formationConfig = options?.formationConfig;
26
+ log('StrandSolicitationService created for party: %s', this.partyId);
27
+ }
28
+ /**
29
+ * Get or create the StrandFormationManager.
30
+ * Lazily initialized to allow configuration after construction.
31
+ */
32
+ getFormationManager() {
33
+ if (!this.formationManager) {
34
+ this.formationManager = new StrandFormationManager({
35
+ disclosureValidator: this.disclosureValidator,
36
+ formationUsageRecorder: this.formationUsageRecorder,
37
+ strandProvisioner: this.strandProvisioner,
38
+ partyId: this.partyId,
39
+ cadrePeerAddrs: this.cadrePeerAddrs,
40
+ config: this.formationConfig
41
+ });
42
+ }
43
+ return this.formationManager;
44
+ }
45
+ /**
46
+ * Register as a responder on a libp2p node.
47
+ * This enables the node to handle incoming strand formation requests.
48
+ */
49
+ registerResponder(node) {
50
+ this.getFormationManager().registerResponder(node);
51
+ log('Registered as responder');
52
+ }
53
+ /**
54
+ * Unregister as a responder from a libp2p node.
55
+ */
56
+ unregisterResponder(node) {
57
+ this.getFormationManager().unregisterResponder(node);
58
+ log('Unregistered as responder');
59
+ }
60
+ /**
61
+ * Form a strand with a responder via an open invitation.
62
+ *
63
+ * Called by the initiator (the party who received an out-of-band invitation).
64
+ * This generates a member key, contacts the responder's cadre, and negotiates
65
+ * strand formation.
66
+ *
67
+ * @param invitation The open invitation (or just token for legacy API)
68
+ * @param disclosure Identity/context information to share with the responder
69
+ * @param node Optional libp2p node for real protocol handling
70
+ * @returns The member key and strand info if successful
71
+ */
72
+ async formStrand(invitation, disclosure, node) {
73
+ // Handle legacy API where just token was passed
74
+ const token = typeof invitation === 'string' ? invitation : invitation.token;
75
+ log('Forming strand with token: %s', token);
76
+ // Generate a new keypair for this strand membership
77
+ const privateKey = await generateKeyPair('Ed25519');
78
+ const peerId = peerIdFromPrivateKey(privateKey);
79
+ const privateKeyBytes = privateKeyToProtobuf(privateKey);
80
+ const memberKey = peerId.toString();
81
+ const invitePrivateKey = uint8ArrayToString(privateKeyBytes, 'base64');
82
+ log('Generated member key: %s', memberKey);
83
+ // If we have a full invitation and a node, use the real protocol
84
+ if (typeof invitation !== 'string' && node) {
85
+ log('Using strand-proto for real protocol handling');
86
+ const result = await this.getFormationManager().formStrand(invitation, { ...disclosure, partyId: memberKey }, node);
87
+ return {
88
+ memberKey,
89
+ invitePrivateKey,
90
+ strandId: result.strandId
91
+ };
92
+ }
93
+ // Fallback: placeholder strandId (for testing without network)
94
+ log('No node provided, using placeholder strandId');
95
+ const strandId = `strand-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
96
+ return {
97
+ memberKey,
98
+ invitePrivateKey,
99
+ strandId
100
+ };
101
+ }
102
+ /**
103
+ * Validate a strand formation request.
104
+ *
105
+ * Called by the responder (the party who created the open invitation) when
106
+ * an initiator contacts them to form a strand. Validates the disclosure
107
+ * and returns a signed approval.
108
+ *
109
+ * @param token The invitation token being used
110
+ * @param disclosure The disclosure from the initiator
111
+ * @returns Validation key and signature if approved
112
+ * @throws If validation fails or token is invalid
113
+ */
114
+ async validateStrandFormation(token, disclosure) {
115
+ log('Validating strand formation for token: %s', token);
116
+ // Check token validity
117
+ if (this.formationUsageRecorder) {
118
+ const tokenCheck = await this.formationUsageRecorder.isTokenValid(token);
119
+ if (!tokenCheck.valid) {
120
+ log('Token invalid or expired: %s', token);
121
+ throw new Error('Invalid or expired token');
122
+ }
123
+ // Check if already used (for single-use tokens)
124
+ const isUsed = await this.formationUsageRecorder.isTokenUsed(token);
125
+ if (isUsed) {
126
+ log('Token already used: %s', token);
127
+ throw new Error('Token has already been used');
128
+ }
129
+ }
130
+ // Validate the disclosure
131
+ if (this.disclosureValidator) {
132
+ const isValid = await this.disclosureValidator.validateDisclosure(token, disclosure);
133
+ if (!isValid) {
134
+ log('Disclosure validation failed for token: %s', token);
135
+ throw new Error('Disclosure validation failed');
136
+ }
137
+ }
138
+ // Sign the formation approval
139
+ if (!this.formationSigner) {
140
+ throw new Error('FormationSigner not configured');
141
+ }
142
+ const { validationKey, validationSignature } = await this.formationSigner.signFormation(token, disclosure);
143
+ log('Formation validated, key: %s', validationKey);
144
+ return {
145
+ validationKey,
146
+ validationSignature
147
+ };
148
+ }
149
+ /**
150
+ * Create an open invitation for others to form strands with this party.
151
+ *
152
+ * @param sAppId The sApp to use for formed strands
153
+ * @param expirationMs How long the invitation is valid (ms from now)
154
+ * @param bootstrap Bootstrap addresses for contacting this party's cadre
155
+ * @returns The open invitation to share out-of-band
156
+ */
157
+ async createOpenInvitation(sAppId, expirationMs, bootstrap) {
158
+ // Generate a unique token
159
+ const token = `invite-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
160
+ const expiration = new Date(Date.now() + expirationMs);
161
+ log('Created open invitation: %s (expires %s)', token, expiration.toISOString());
162
+ return {
163
+ token,
164
+ sAppId,
165
+ expiration,
166
+ bootstrap
167
+ };
168
+ }
169
+ /**
170
+ * Record that a formation was completed successfully.
171
+ * Called after strand provisioning to track usage.
172
+ */
173
+ async recordFormationComplete(token, initiatorKey, strandId) {
174
+ if (this.formationUsageRecorder) {
175
+ await this.formationUsageRecorder.recordUsage(token, initiatorKey, strandId);
176
+ log('Recorded formation usage: token=%s strand=%s', token, strandId);
177
+ }
178
+ }
179
+ }
180
+ //# sourceMappingURL=strand-solicitation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strand-solicitation.js","sourceRoot":"","sources":["../src/strand-solicitation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ import type { StrandFilter, StrandRow } from './types.js';
2
+ /**
3
+ * Extended strand row that includes the sAppId for filtering purposes.
4
+ * The sAppId is provided by the hosting application, not from the control network.
5
+ */
6
+ export interface StrandRowWithApp extends StrandRow {
7
+ /** sApp ID if known (for filtering) */
8
+ sAppId?: string;
9
+ }
10
+ /**
11
+ * Callback for strand changes
12
+ */
13
+ export interface StrandWatcherCallbacks {
14
+ onStrandAdded: (strand: StrandRow) => Promise<void>;
15
+ onStrandRemoved: (strandId: string) => Promise<void>;
16
+ }
17
+ /**
18
+ * Interface for querying strands from control network
19
+ */
20
+ export interface StrandQueryable {
21
+ queryStrands(): Promise<StrandRow[]>;
22
+ }
23
+ /**
24
+ * Interface for looking up sAppId for a strand (for filtering)
25
+ */
26
+ export interface SAppIdLookup {
27
+ /** Get the sAppId for a strand, if known */
28
+ getSAppId(strandId: string): string | undefined;
29
+ }
30
+ /**
31
+ * Watches the control network's Strand table for changes and triggers
32
+ * strand instance start/stop via callbacks.
33
+ *
34
+ * Uses polling until Optimystic supports reactive subscriptions.
35
+ */
36
+ export declare class StrandWatcher {
37
+ private readonly filter;
38
+ private readonly pollInterval;
39
+ private readonly callbacks;
40
+ private readonly queryable;
41
+ private readonly sAppIdLookup?;
42
+ private knownStrands;
43
+ private pollTimer;
44
+ private running;
45
+ constructor(queryable: StrandQueryable, callbacks: StrandWatcherCallbacks, filter?: StrandFilter, pollInterval?: number, sAppIdLookup?: SAppIdLookup);
46
+ /**
47
+ * Check if a strand passes the current filter
48
+ */
49
+ private passesFilter;
50
+ /**
51
+ * Poll for strand changes
52
+ */
53
+ private poll;
54
+ /**
55
+ * Start watching for strand changes
56
+ */
57
+ start(): Promise<void>;
58
+ /**
59
+ * Stop watching for strand changes
60
+ */
61
+ stop(): Promise<void>;
62
+ /**
63
+ * Get currently known strands
64
+ */
65
+ getKnownStrands(): Map<string, StrandRow>;
66
+ /**
67
+ * Force an immediate poll (useful for testing)
68
+ */
69
+ forcePoll(): Promise<void>;
70
+ }
@@ -0,0 +1,136 @@
1
+ import debug from 'debug';
2
+ const log = debug('sereus:cadre:strand-watcher');
3
+ /**
4
+ * Watches the control network's Strand table for changes and triggers
5
+ * strand instance start/stop via callbacks.
6
+ *
7
+ * Uses polling until Optimystic supports reactive subscriptions.
8
+ */
9
+ export class StrandWatcher {
10
+ constructor(queryable, callbacks, filter = { mode: 'all' }, pollInterval = 5000, sAppIdLookup) {
11
+ this.knownStrands = new Map();
12
+ this.pollTimer = null;
13
+ this.running = false;
14
+ this.queryable = queryable;
15
+ this.callbacks = callbacks;
16
+ this.filter = filter;
17
+ this.pollInterval = pollInterval;
18
+ this.sAppIdLookup = sAppIdLookup;
19
+ log('StrandWatcher created with filter: %o, interval: %dms', filter, pollInterval);
20
+ }
21
+ /**
22
+ * Check if a strand passes the current filter
23
+ */
24
+ passesFilter(strand) {
25
+ switch (this.filter.mode) {
26
+ case 'all':
27
+ return true;
28
+ case 'none':
29
+ return false;
30
+ case 'strandId':
31
+ return strand.Id === this.filter.strandId;
32
+ case 'sAppId': {
33
+ // Look up the sAppId for this strand
34
+ const sAppId = this.sAppIdLookup?.getSAppId(strand.Id);
35
+ if (sAppId === undefined) {
36
+ // If no sAppId lookup configured or unknown strand, pass through
37
+ // The strand will need to be re-evaluated when sApp info is available
38
+ log('sAppId unknown for strand %s - deferring filter decision', strand.Id);
39
+ return true;
40
+ }
41
+ const matches = sAppId === this.filter.sAppId;
42
+ log('sAppId filter: strand %s has sAppId %s, filter wants %s, match=%s', strand.Id, sAppId, this.filter.sAppId, matches);
43
+ return matches;
44
+ }
45
+ default:
46
+ return true;
47
+ }
48
+ }
49
+ /**
50
+ * Poll for strand changes
51
+ */
52
+ async poll() {
53
+ if (!this.running)
54
+ return;
55
+ try {
56
+ const currentStrands = await this.queryable.queryStrands();
57
+ const currentMap = new Map(currentStrands.map(s => [s.Id, s]));
58
+ // Find added strands
59
+ for (const strand of currentStrands) {
60
+ if (!this.knownStrands.has(strand.Id) && this.passesFilter(strand)) {
61
+ log('Strand added: %s', strand.Id);
62
+ this.knownStrands.set(strand.Id, strand);
63
+ try {
64
+ await this.callbacks.onStrandAdded(strand);
65
+ }
66
+ catch (error) {
67
+ log('Error handling strand add for %s: %o', strand.Id, error);
68
+ }
69
+ }
70
+ }
71
+ // Find removed strands
72
+ for (const [strandId] of this.knownStrands) {
73
+ if (!currentMap.has(strandId)) {
74
+ log('Strand removed: %s', strandId);
75
+ this.knownStrands.delete(strandId);
76
+ try {
77
+ await this.callbacks.onStrandRemoved(strandId);
78
+ }
79
+ catch (error) {
80
+ log('Error handling strand remove for %s: %o', strandId, error);
81
+ }
82
+ }
83
+ }
84
+ }
85
+ catch (error) {
86
+ log('Error polling strands: %o', error);
87
+ }
88
+ }
89
+ /**
90
+ * Start watching for strand changes
91
+ */
92
+ async start() {
93
+ if (this.running) {
94
+ log('StrandWatcher already running');
95
+ return;
96
+ }
97
+ log('Starting StrandWatcher');
98
+ this.running = true;
99
+ // Do an initial poll immediately
100
+ await this.poll();
101
+ // Set up periodic polling
102
+ this.pollTimer = setInterval(() => {
103
+ void this.poll();
104
+ }, this.pollInterval);
105
+ log('StrandWatcher started');
106
+ }
107
+ /**
108
+ * Stop watching for strand changes
109
+ */
110
+ async stop() {
111
+ if (!this.running) {
112
+ return;
113
+ }
114
+ log('Stopping StrandWatcher');
115
+ this.running = false;
116
+ if (this.pollTimer) {
117
+ clearInterval(this.pollTimer);
118
+ this.pollTimer = null;
119
+ }
120
+ this.knownStrands.clear();
121
+ log('StrandWatcher stopped');
122
+ }
123
+ /**
124
+ * Get currently known strands
125
+ */
126
+ getKnownStrands() {
127
+ return new Map(this.knownStrands);
128
+ }
129
+ /**
130
+ * Force an immediate poll (useful for testing)
131
+ */
132
+ async forcePoll() {
133
+ await this.poll();
134
+ }
135
+ }
136
+ //# sourceMappingURL=strand-watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strand-watcher.js","sourceRoot":"","sources":["../src/strand-watcher.ts"],"names":[],"mappings":"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"}