@learncard/learn-cloud-plugin 2.3.38 → 2.3.40

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.
@@ -9,6 +9,7 @@ const makeMockClient = (learnCloudDid = 'did:key:z6MkLearnCloud') => ({
9
9
  getDid: { query: vi.fn().mockResolvedValue(learnCloudDid) },
10
10
  },
11
11
  customStorage: {
12
+ count: { query: vi.fn().mockResolvedValue(0) },
12
13
  create: { mutate: vi.fn().mockResolvedValue('lc:cloud:credential') },
13
14
  },
14
15
  storage: {
@@ -45,8 +46,8 @@ const makeLearnCard = (did = 'did:key:z6MkHolder') => ({
45
46
  invoke: {
46
47
  getDidAuthVp: vi.fn().mockResolvedValue('did-auth-jwt'),
47
48
  decryptDagJwe: vi.fn(async value => value),
48
- createDagJwe: vi.fn(async (value, recipients) => ({ value, recipients })),
49
- hash: vi.fn().mockResolvedValue('hashed-field'),
49
+ createDagJwe: vi.fn(async () => ({ ciphertext: 'encrypted' })),
50
+ hash: vi.fn(async value => `hash:${value}`),
50
51
  },
51
52
  debug: vi.fn(),
52
53
  });
@@ -57,38 +58,205 @@ describe('LearnCloud Plugin', () => {
57
58
  mockGetClient.mockResolvedValue(mockClient);
58
59
  });
59
60
 
60
- it('exposes a function', () => {
61
- expect(getLearnCloudPlugin).toBeDefined();
61
+ it('defers remote identity requests until a method needs them', async () => {
62
+ await getLearnCloudPlugin(makeLearnCard() as never, 'https://cloud.example');
63
+
64
+ expect(mockClient.user.getDids.query).not.toHaveBeenCalled();
65
+ expect(mockClient.utilities.getDid.query).not.toHaveBeenCalled();
66
+ });
67
+
68
+ it('does not load associated DIDs when automatic association is disabled', async () => {
69
+ const learnCard = makeLearnCard();
70
+ const plugin = await getLearnCloudPlugin(
71
+ learnCard as never,
72
+ 'https://cloud.example',
73
+ [],
74
+ [],
75
+ false
76
+ );
77
+
78
+ await plugin.methods.learnCloudCount(
79
+ makeLearnCard('did:web:holder.example') as never,
80
+ undefined as never,
81
+ false
82
+ );
83
+
84
+ expect(mockClient.user.getDids.query).not.toHaveBeenCalled();
85
+ expect(mockClient.customStorage.count.query).toHaveBeenCalledWith({
86
+ includeAssociatedDids: false,
87
+ });
62
88
  });
63
89
 
64
- it('refreshes the LearnCloud DID after switching authenticated clients', async () => {
65
- const initialClient = makeMockClient('did:web:cloud-one.example');
66
- const switchedClient = makeMockClient('did:web:cloud-two.example');
67
- mockGetClient.mockResolvedValueOnce(initialClient).mockResolvedValueOnce(switchedClient);
90
+ it('refreshes the LearnCloud recipient DID after the active wallet changes', async () => {
91
+ const firstClient = {
92
+ ...mockClient,
93
+ utilities: {
94
+ getDid: { query: vi.fn().mockResolvedValue('did:key:z6MkFirstLearnCloud') },
95
+ },
96
+ customStorage: {
97
+ ...mockClient.customStorage,
98
+ create: { mutate: vi.fn().mockResolvedValue('first-record') },
99
+ },
100
+ };
101
+ const secondClient = {
102
+ ...mockClient,
103
+ utilities: {
104
+ getDid: { query: vi.fn().mockResolvedValue('did:key:z6MkSecondLearnCloud') },
105
+ },
106
+ customStorage: {
107
+ ...mockClient.customStorage,
108
+ create: { mutate: vi.fn().mockResolvedValue('second-record') },
109
+ },
110
+ };
111
+ mockGetClient.mockResolvedValueOnce(firstClient).mockResolvedValueOnce(secondClient);
68
112
 
69
- const initialLearnCard = makeLearnCard('did:key:holder-one');
70
- const switchedLearnCard = makeLearnCard('did:key:holder-two');
113
+ const firstLearnCard = makeLearnCard('did:key:z6MkFirstHolder');
114
+ const secondLearnCard = makeLearnCard('did:key:z6MkSecondHolder');
71
115
  const plugin = await getLearnCloudPlugin(
72
- initialLearnCard as never,
116
+ firstLearnCard as never,
73
117
  'https://cloud.example',
74
118
  [],
75
119
  [],
76
120
  false
77
121
  );
78
122
 
79
- await plugin.methods.learnCloudCreate(initialLearnCard as never, { id: 'first' });
80
- await plugin.methods.learnCloudCreate(switchedLearnCard as never, { id: 'second' });
123
+ await plugin.methods.learnCloudCreate(firstLearnCard as never, { id: 'first' });
124
+ await plugin.methods.learnCloudCreate(secondLearnCard as never, { id: 'second' });
81
125
 
82
- expect(initialClient.utilities.getDid.query).toHaveBeenCalledTimes(1);
83
- expect(switchedClient.utilities.getDid.query).toHaveBeenCalledTimes(1);
84
- expect(initialLearnCard.invoke.createDagJwe).toHaveBeenLastCalledWith(expect.any(Object), [
85
- 'did:web:cloud-one.example',
126
+ expect(firstClient.utilities.getDid.query).toHaveBeenCalledTimes(1);
127
+ expect(secondClient.utilities.getDid.query).toHaveBeenCalledTimes(1);
128
+ expect(firstLearnCard.invoke.createDagJwe).toHaveBeenLastCalledWith(expect.anything(), [
129
+ 'did:key:z6MkFirstLearnCloud',
86
130
  ]);
87
- expect(switchedLearnCard.invoke.createDagJwe).toHaveBeenLastCalledWith(expect.any(Object), [
88
- 'did:web:cloud-two.example',
131
+ expect(secondLearnCard.invoke.createDagJwe).toHaveBeenLastCalledWith(expect.anything(), [
132
+ 'did:key:z6MkSecondLearnCloud',
89
133
  ]);
90
134
  });
91
135
 
136
+ it('keeps an in-flight write bound to the wallet client that encrypted it', async () => {
137
+ const firstClient = {
138
+ ...mockClient,
139
+ utilities: {
140
+ getDid: { query: vi.fn().mockResolvedValue('did:key:z6MkFirstLearnCloud') },
141
+ },
142
+ customStorage: {
143
+ ...mockClient.customStorage,
144
+ create: { mutate: vi.fn().mockResolvedValue('first-record') },
145
+ },
146
+ };
147
+ const secondClient = {
148
+ ...mockClient,
149
+ utilities: {
150
+ getDid: { query: vi.fn().mockResolvedValue('did:key:z6MkSecondLearnCloud') },
151
+ },
152
+ customStorage: {
153
+ ...mockClient.customStorage,
154
+ create: { mutate: vi.fn().mockResolvedValue('second-record') },
155
+ },
156
+ };
157
+ mockGetClient.mockResolvedValueOnce(firstClient).mockResolvedValueOnce(secondClient);
158
+
159
+ const firstLearnCard = makeLearnCard('did:key:z6MkFirstHolder');
160
+ const secondLearnCard = makeLearnCard('did:key:z6MkSecondHolder');
161
+ let releaseFirstEncryption: (() => void) | undefined;
162
+ let markFirstEncryptionStarted: (() => void) | undefined;
163
+ const firstEncryptionStarted = new Promise<void>(resolve => {
164
+ markFirstEncryptionStarted = resolve;
165
+ });
166
+ const firstEncryptionReleased = new Promise<void>(resolve => {
167
+ releaseFirstEncryption = resolve;
168
+ });
169
+ let firstEncryptionCalls = 0;
170
+
171
+ firstLearnCard.invoke.createDagJwe.mockImplementation(async () => {
172
+ firstEncryptionCalls += 1;
173
+
174
+ if (firstEncryptionCalls === 2) {
175
+ markFirstEncryptionStarted?.();
176
+ await firstEncryptionReleased;
177
+ }
178
+
179
+ return { ciphertext: `first-${firstEncryptionCalls}` };
180
+ });
181
+
182
+ const plugin = await getLearnCloudPlugin(
183
+ firstLearnCard as never,
184
+ 'https://cloud.example',
185
+ [],
186
+ [],
187
+ false
188
+ );
189
+ const firstWrite = plugin.methods.learnCloudCreate(firstLearnCard as never, {
190
+ id: 'first',
191
+ });
192
+
193
+ await firstEncryptionStarted;
194
+
195
+ const secondWrite = plugin.methods.learnCloudCreate(secondLearnCard as never, {
196
+ id: 'second',
197
+ });
198
+
199
+ await secondWrite;
200
+ releaseFirstEncryption?.();
201
+ await firstWrite;
202
+
203
+ expect(firstClient.customStorage.create.mutate).toHaveBeenCalledWith({
204
+ item: { ciphertext: 'first-2' },
205
+ });
206
+ expect(secondClient.customStorage.create.mutate).toHaveBeenCalledTimes(1);
207
+ });
208
+
209
+ it('keys cross-cloud clients by both URL and active wallet DID', async () => {
210
+ const otherAuthCallbacks: Array<(challenge: string) => Promise<string>> = [];
211
+ const crossCloudClient = {
212
+ ...mockClient,
213
+ storage: {
214
+ ...mockClient.storage,
215
+ resolve: { query: vi.fn().mockResolvedValue(makeW3cVc()) },
216
+ },
217
+ };
218
+
219
+ mockGetClient.mockImplementation(
220
+ async (clientUrl: string, authenticate: (challenge: string) => Promise<string>) => {
221
+ if (clientUrl === 'https://other.example') {
222
+ otherAuthCallbacks.push(authenticate);
223
+
224
+ return crossCloudClient;
225
+ }
226
+
227
+ return mockClient;
228
+ }
229
+ );
230
+
231
+ const firstLearnCard = makeLearnCard('did:key:z6MkFirstHolder');
232
+ const secondLearnCard = makeLearnCard('did:key:z6MkSecondHolder');
233
+ const plugin = await getLearnCloudPlugin(
234
+ firstLearnCard as never,
235
+ 'https://cloud.example',
236
+ [],
237
+ [],
238
+ false
239
+ );
240
+ const uri = 'lc:cloud:other.example:credential:1';
241
+
242
+ await plugin.read.get(firstLearnCard as never, uri);
243
+ await plugin.read.get(secondLearnCard as never, uri);
244
+
245
+ expect(otherAuthCallbacks).toHaveLength(2);
246
+
247
+ await otherAuthCallbacks[0]!('first-challenge');
248
+ await otherAuthCallbacks[1]!('second-challenge');
249
+
250
+ expect(firstLearnCard.invoke.getDidAuthVp).toHaveBeenCalledWith({
251
+ proofFormat: 'jwt',
252
+ challenge: 'first-challenge',
253
+ });
254
+ expect(secondLearnCard.invoke.getDidAuthVp).toHaveBeenCalledWith({
255
+ proofFormat: 'jwt',
256
+ challenge: 'second-challenge',
257
+ });
258
+ });
259
+
92
260
  it('projects envelope-backed credentials in learnCloudBatchResolve', async () => {
93
261
  const compact =
94
262
  'eyJhbGciOiJFZERTQSIsInR5cCI6ImRjK3NkLWp3dCJ9.eyJpc3MiOiJkaWQ6d2ViOmlzc3Vlci5leGFtcGxlIiwiaWF0IjoxNzAwMDAwMDAwLCJ2Y3QiOiJodHRwczovL2V4YW1wbGUuY29tL2NyZWRlbnRpYWxzL2VtcGxveW1lbnQiLCJuYW1lIjoiQWRhIn0.AAAA~';