@alfe.ai/openclaw-identity 0.0.19 → 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/dist/plugin2.cjs CHANGED
@@ -147,6 +147,75 @@ function setCachedResolve(key, value) {
147
147
  expiresAt: Date.now() + RESOLVE_CACHE_TTL_MS
148
148
  });
149
149
  }
150
+ function resolveCachedSession(params) {
151
+ const conversationId = params.conversationId;
152
+ const explicitClaimed = params.claimedIdentityId;
153
+ if (!conversationId) return {
154
+ hit: false,
155
+ conversationId: null
156
+ };
157
+ const cached = getCachedResolve(conversationId);
158
+ if (cached?.identityId == null) return {
159
+ hit: false,
160
+ conversationId,
161
+ error: "session_identity_unavailable: inbound identity cache miss for this conversationId. Pass claimedIdentityId explicitly, or wait for the next inbound message."
162
+ };
163
+ if (explicitClaimed && explicitClaimed !== cached.identityId) return {
164
+ hit: false,
165
+ conversationId,
166
+ error: "claimedIdentityId conflicts with the inbound session identity. Either omit claimedIdentityId (recommended) or remove conversationId."
167
+ };
168
+ return {
169
+ hit: true,
170
+ cached: {
171
+ ...cached,
172
+ identityId: cached.identityId
173
+ }
174
+ };
175
+ }
176
+ /**
177
+ * Resolution order for the request flow (needs the full requester tuple):
178
+ * 1. conversationId hits cache → derive all four from cached entry.
179
+ * 2. conversationId set but cache miss → reject session_identity_unavailable.
180
+ * 3. No conversationId AND no claimedIdentityId → reject missing_identity.
181
+ * 4. claimedIdentityId + full requester tuple supplied → legacy explicit
182
+ * flow. Layer 1's server-side `no_op_already_verified` guard catches
183
+ * the common misuse case (looking up an existing identity by name).
184
+ */
185
+ function resolveRequesterContext(params) {
186
+ const session = resolveCachedSession(params);
187
+ if (session.hit) return {
188
+ claimedIdentityId: session.cached.identityId,
189
+ requestingIdentityId: session.cached.identityId,
190
+ requestingProvider: session.cached.senderProvider,
191
+ requestingPlatformId: session.cached.senderPlatformId
192
+ };
193
+ if (session.conversationId) return { error: session.error };
194
+ const claimedIdentityId = params.claimedIdentityId;
195
+ const requestingIdentityId = params.requestingIdentityId;
196
+ const requestingProvider = params.requestingProvider;
197
+ const requestingPlatformId = params.requestingPlatformId;
198
+ if (!claimedIdentityId) return { error: "missing_identity: pass conversationId (recommended — auto-binds to the inbound sender) or claimedIdentityId + requestingIdentityId + requestingProvider + requestingPlatformId explicitly." };
199
+ if (!requestingIdentityId || !requestingProvider || !requestingPlatformId) return { error: "missing_identity: when claimedIdentityId is supplied without conversationId, requestingIdentityId / requestingProvider / requestingPlatformId are also required. Prefer passing conversationId so all four are auto-derived." };
200
+ return {
201
+ claimedIdentityId,
202
+ requestingIdentityId,
203
+ requestingProvider,
204
+ requestingPlatformId
205
+ };
206
+ }
207
+ /**
208
+ * Confirm flow only needs `claimedIdentityId` — same conflict / cache-miss
209
+ * branches as the requester resolver, but no requester tuple.
210
+ */
211
+ function resolveClaimedIdentity(params) {
212
+ const session = resolveCachedSession(params);
213
+ if (session.hit) return { claimedIdentityId: session.cached.identityId };
214
+ if (session.conversationId) return { error: session.error };
215
+ const claimedIdentityId = params.claimedIdentityId;
216
+ if (!claimedIdentityId) return { error: "missing_identity: pass conversationId (recommended — auto-binds to the inbound sender) or claimedIdentityId explicitly." };
217
+ return { claimedIdentityId };
218
+ }
150
219
  function ok(data) {
151
220
  return {
152
221
  content: [{
@@ -330,12 +399,13 @@ const plugin = {
330
399
  }),
331
400
  defineTool({
332
401
  name: "request_identity_verification",
333
- description: "Send a verification phrase to verify the user controls an email or mobile endpoint. Use this when the user has just told you their email or mobile number and you want them to confirm it. Pass exactly one of `contactEmail` or `contactMobile`. The person must relay the phrase back via `confirm_identity_verification`.",
402
+ description: "Send a verification phrase to the inbound user's email or mobile to prove they own that contact endpoint. Use when the user has just told you their email or mobile number and you want them to confirm it. PASS conversationId (from the inbound message metadata) that auto-binds the verification to the inbound sender's identity (the WhatsApp / SMS / Discord / Google Chat user who just spoke). Any merge into a pre-existing account with the same contact happens automatically. Do NOT pass claimedIdentityId yourself unless you have a specific reason to verify a different identity than the inbound sender; looking up an identity by name and passing it here is a footgun and the server will reject with no_op_already_verified.",
334
403
  parameters: _sinclair_typebox.Type.Object({
335
- claimedIdentityId: _sinclair_typebox.Type.String({ description: "Identity being claimed" }),
336
- requestingIdentityId: _sinclair_typebox.Type.String({ description: "Identity of the person making the claim" }),
337
- requestingProvider: _sinclair_typebox.Type.String({ description: "Provider the requester is on (discord, slack, chat, etc.)" }),
338
- requestingPlatformId: _sinclair_typebox.Type.String({ description: "Requester's provider-specific user ID" }),
404
+ conversationId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Conversation ID from the inbound message — the daemon attaches this to every message. Pass it to auto-bind the verification to the inbound sender's identity. When set you should NOT also pass claimedIdentityId / requestingIdentityId / requestingProvider / requestingPlatformId." })),
405
+ claimedIdentityId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Override for the inbound sender's identity. NEVER set this from a lookup_identity result — that's a footgun (the verification will no-op). Leave unset and pass conversationId so the inbound identity is used automatically. Only set this for explicit non-inbound verification flows (rare — dashboard / admin only)." })),
406
+ requestingIdentityId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Identity of the person making the claim. Auto-derived from conversationId; only set explicitly when conversationId is omitted." })),
407
+ requestingProvider: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Provider the requester is on (discord, slack, whatsapp, etc.). Auto-derived from conversationId; only set explicitly when conversationId is omitted." })),
408
+ requestingPlatformId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Requester's provider-specific user ID. Auto-derived from conversationId; only set explicitly when conversationId is omitted." })),
339
409
  contactEmail: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Email to verify (mutually exclusive with contactMobile)" })),
340
410
  contactMobile: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "E.164 mobile to verify (mutually exclusive with contactEmail)" })),
341
411
  preferredChannel: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "When neither contactEmail nor contactMobile is provided, pick which existing verified contact to deliver to: 'mobile' or 'email'" }))
@@ -351,11 +421,13 @@ const plugin = {
351
421
  channel: "mobile",
352
422
  value: contactMobile
353
423
  } : void 0;
424
+ const resolved = resolveRequesterContext(params);
425
+ if ("error" in resolved) return Promise.resolve({ error: resolved.error });
354
426
  return client.requestIdentityVerification({
355
- claimedIdentityId: params.claimedIdentityId,
356
- requestingIdentityId: params.requestingIdentityId,
357
- requestingProvider: params.requestingProvider,
358
- requestingPlatformId: params.requestingPlatformId,
427
+ claimedIdentityId: resolved.claimedIdentityId,
428
+ requestingIdentityId: resolved.requestingIdentityId,
429
+ requestingProvider: resolved.requestingProvider,
430
+ requestingPlatformId: resolved.requestingPlatformId,
359
431
  preferredChannel: params.preferredChannel,
360
432
  contact
361
433
  });
@@ -363,17 +435,22 @@ const plugin = {
363
435
  }),
364
436
  defineTool({
365
437
  name: "confirm_identity_verification",
366
- description: "Confirm a verification by submitting the phrase the user received. Returns `{ verified, identityId, action }` where `action` is 'merged' (the verified contact already lived on another identity, which is now the survivor) or 'contact_verified' (the contact was attached to the claimed identity).",
438
+ description: "Confirm a verification by submitting the phrase the user received. Returns `{ verified, identityId, action }` where `action` is 'merged' (the verified contact already lived on another identity, which is now the survivor) or 'contact_verified' (the contact was attached to the claimed identity). PASS the same conversationId you used on request_identity_verification — claimedIdentityId is auto-derived from it.",
367
439
  parameters: _sinclair_typebox.Type.Object({
368
- claimedIdentityId: _sinclair_typebox.Type.String({ description: "Identity being claimed (matches request)" }),
440
+ conversationId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Conversation ID from the inbound message — same one passed to request_identity_verification. Auto-binds claimedIdentityId to the inbound sender." })),
441
+ claimedIdentityId: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ description: "Override for the inbound sender's identity. Must match the value used on the request. Leave unset and pass conversationId." })),
369
442
  verificationId: _sinclair_typebox.Type.String({ description: "Verification ID returned from request_identity_verification" }),
370
443
  phrase: _sinclair_typebox.Type.String({ description: "Three-word phrase the person received via mobile or email" })
371
444
  }),
372
- handler: (params) => client.confirmIdentityVerification({
373
- claimedIdentityId: params.claimedIdentityId,
374
- verificationId: params.verificationId,
375
- phrase: params.phrase
376
- })
445
+ handler: (params) => {
446
+ const resolved = resolveClaimedIdentity(params);
447
+ if ("error" in resolved) return Promise.resolve({ error: resolved.error });
448
+ return client.confirmIdentityVerification({
449
+ claimedIdentityId: resolved.claimedIdentityId,
450
+ verificationId: params.verificationId,
451
+ phrase: params.phrase
452
+ });
453
+ }
377
454
  })
378
455
  ];
379
456
  for (const tool of tools) api.registerTool(tool);
@@ -414,7 +491,9 @@ const plugin = {
414
491
  setCachedResolve(cacheKey, {
415
492
  identityId: r.identityId,
416
493
  status: r.status,
417
- permissions: [...r.permissions, ...forwarded]
494
+ permissions: [...r.permissions, ...forwarded],
495
+ senderProvider: provider,
496
+ senderPlatformId: senderId
418
497
  });
419
498
  if (r.identityId == null) {
420
499
  log.warn(`Identity not provisioned for ${provider}:${senderId} — blocking inbound message`);
package/dist/plugin2.js CHANGED
@@ -147,6 +147,75 @@ function setCachedResolve(key, value) {
147
147
  expiresAt: Date.now() + RESOLVE_CACHE_TTL_MS
148
148
  });
149
149
  }
150
+ function resolveCachedSession(params) {
151
+ const conversationId = params.conversationId;
152
+ const explicitClaimed = params.claimedIdentityId;
153
+ if (!conversationId) return {
154
+ hit: false,
155
+ conversationId: null
156
+ };
157
+ const cached = getCachedResolve(conversationId);
158
+ if (cached?.identityId == null) return {
159
+ hit: false,
160
+ conversationId,
161
+ error: "session_identity_unavailable: inbound identity cache miss for this conversationId. Pass claimedIdentityId explicitly, or wait for the next inbound message."
162
+ };
163
+ if (explicitClaimed && explicitClaimed !== cached.identityId) return {
164
+ hit: false,
165
+ conversationId,
166
+ error: "claimedIdentityId conflicts with the inbound session identity. Either omit claimedIdentityId (recommended) or remove conversationId."
167
+ };
168
+ return {
169
+ hit: true,
170
+ cached: {
171
+ ...cached,
172
+ identityId: cached.identityId
173
+ }
174
+ };
175
+ }
176
+ /**
177
+ * Resolution order for the request flow (needs the full requester tuple):
178
+ * 1. conversationId hits cache → derive all four from cached entry.
179
+ * 2. conversationId set but cache miss → reject session_identity_unavailable.
180
+ * 3. No conversationId AND no claimedIdentityId → reject missing_identity.
181
+ * 4. claimedIdentityId + full requester tuple supplied → legacy explicit
182
+ * flow. Layer 1's server-side `no_op_already_verified` guard catches
183
+ * the common misuse case (looking up an existing identity by name).
184
+ */
185
+ function resolveRequesterContext(params) {
186
+ const session = resolveCachedSession(params);
187
+ if (session.hit) return {
188
+ claimedIdentityId: session.cached.identityId,
189
+ requestingIdentityId: session.cached.identityId,
190
+ requestingProvider: session.cached.senderProvider,
191
+ requestingPlatformId: session.cached.senderPlatformId
192
+ };
193
+ if (session.conversationId) return { error: session.error };
194
+ const claimedIdentityId = params.claimedIdentityId;
195
+ const requestingIdentityId = params.requestingIdentityId;
196
+ const requestingProvider = params.requestingProvider;
197
+ const requestingPlatformId = params.requestingPlatformId;
198
+ if (!claimedIdentityId) return { error: "missing_identity: pass conversationId (recommended — auto-binds to the inbound sender) or claimedIdentityId + requestingIdentityId + requestingProvider + requestingPlatformId explicitly." };
199
+ if (!requestingIdentityId || !requestingProvider || !requestingPlatformId) return { error: "missing_identity: when claimedIdentityId is supplied without conversationId, requestingIdentityId / requestingProvider / requestingPlatformId are also required. Prefer passing conversationId so all four are auto-derived." };
200
+ return {
201
+ claimedIdentityId,
202
+ requestingIdentityId,
203
+ requestingProvider,
204
+ requestingPlatformId
205
+ };
206
+ }
207
+ /**
208
+ * Confirm flow only needs `claimedIdentityId` — same conflict / cache-miss
209
+ * branches as the requester resolver, but no requester tuple.
210
+ */
211
+ function resolveClaimedIdentity(params) {
212
+ const session = resolveCachedSession(params);
213
+ if (session.hit) return { claimedIdentityId: session.cached.identityId };
214
+ if (session.conversationId) return { error: session.error };
215
+ const claimedIdentityId = params.claimedIdentityId;
216
+ if (!claimedIdentityId) return { error: "missing_identity: pass conversationId (recommended — auto-binds to the inbound sender) or claimedIdentityId explicitly." };
217
+ return { claimedIdentityId };
218
+ }
150
219
  function ok(data) {
151
220
  return {
152
221
  content: [{
@@ -330,12 +399,13 @@ const plugin = {
330
399
  }),
331
400
  defineTool({
332
401
  name: "request_identity_verification",
333
- description: "Send a verification phrase to verify the user controls an email or mobile endpoint. Use this when the user has just told you their email or mobile number and you want them to confirm it. Pass exactly one of `contactEmail` or `contactMobile`. The person must relay the phrase back via `confirm_identity_verification`.",
402
+ description: "Send a verification phrase to the inbound user's email or mobile to prove they own that contact endpoint. Use when the user has just told you their email or mobile number and you want them to confirm it. PASS conversationId (from the inbound message metadata) that auto-binds the verification to the inbound sender's identity (the WhatsApp / SMS / Discord / Google Chat user who just spoke). Any merge into a pre-existing account with the same contact happens automatically. Do NOT pass claimedIdentityId yourself unless you have a specific reason to verify a different identity than the inbound sender; looking up an identity by name and passing it here is a footgun and the server will reject with no_op_already_verified.",
334
403
  parameters: Type.Object({
335
- claimedIdentityId: Type.String({ description: "Identity being claimed" }),
336
- requestingIdentityId: Type.String({ description: "Identity of the person making the claim" }),
337
- requestingProvider: Type.String({ description: "Provider the requester is on (discord, slack, chat, etc.)" }),
338
- requestingPlatformId: Type.String({ description: "Requester's provider-specific user ID" }),
404
+ conversationId: Type.Optional(Type.String({ description: "Conversation ID from the inbound message — the daemon attaches this to every message. Pass it to auto-bind the verification to the inbound sender's identity. When set you should NOT also pass claimedIdentityId / requestingIdentityId / requestingProvider / requestingPlatformId." })),
405
+ claimedIdentityId: Type.Optional(Type.String({ description: "Override for the inbound sender's identity. NEVER set this from a lookup_identity result — that's a footgun (the verification will no-op). Leave unset and pass conversationId so the inbound identity is used automatically. Only set this for explicit non-inbound verification flows (rare — dashboard / admin only)." })),
406
+ requestingIdentityId: Type.Optional(Type.String({ description: "Identity of the person making the claim. Auto-derived from conversationId; only set explicitly when conversationId is omitted." })),
407
+ requestingProvider: Type.Optional(Type.String({ description: "Provider the requester is on (discord, slack, whatsapp, etc.). Auto-derived from conversationId; only set explicitly when conversationId is omitted." })),
408
+ requestingPlatformId: Type.Optional(Type.String({ description: "Requester's provider-specific user ID. Auto-derived from conversationId; only set explicitly when conversationId is omitted." })),
339
409
  contactEmail: Type.Optional(Type.String({ description: "Email to verify (mutually exclusive with contactMobile)" })),
340
410
  contactMobile: Type.Optional(Type.String({ description: "E.164 mobile to verify (mutually exclusive with contactEmail)" })),
341
411
  preferredChannel: Type.Optional(Type.String({ description: "When neither contactEmail nor contactMobile is provided, pick which existing verified contact to deliver to: 'mobile' or 'email'" }))
@@ -351,11 +421,13 @@ const plugin = {
351
421
  channel: "mobile",
352
422
  value: contactMobile
353
423
  } : void 0;
424
+ const resolved = resolveRequesterContext(params);
425
+ if ("error" in resolved) return Promise.resolve({ error: resolved.error });
354
426
  return client.requestIdentityVerification({
355
- claimedIdentityId: params.claimedIdentityId,
356
- requestingIdentityId: params.requestingIdentityId,
357
- requestingProvider: params.requestingProvider,
358
- requestingPlatformId: params.requestingPlatformId,
427
+ claimedIdentityId: resolved.claimedIdentityId,
428
+ requestingIdentityId: resolved.requestingIdentityId,
429
+ requestingProvider: resolved.requestingProvider,
430
+ requestingPlatformId: resolved.requestingPlatformId,
359
431
  preferredChannel: params.preferredChannel,
360
432
  contact
361
433
  });
@@ -363,17 +435,22 @@ const plugin = {
363
435
  }),
364
436
  defineTool({
365
437
  name: "confirm_identity_verification",
366
- description: "Confirm a verification by submitting the phrase the user received. Returns `{ verified, identityId, action }` where `action` is 'merged' (the verified contact already lived on another identity, which is now the survivor) or 'contact_verified' (the contact was attached to the claimed identity).",
438
+ description: "Confirm a verification by submitting the phrase the user received. Returns `{ verified, identityId, action }` where `action` is 'merged' (the verified contact already lived on another identity, which is now the survivor) or 'contact_verified' (the contact was attached to the claimed identity). PASS the same conversationId you used on request_identity_verification — claimedIdentityId is auto-derived from it.",
367
439
  parameters: Type.Object({
368
- claimedIdentityId: Type.String({ description: "Identity being claimed (matches request)" }),
440
+ conversationId: Type.Optional(Type.String({ description: "Conversation ID from the inbound message — same one passed to request_identity_verification. Auto-binds claimedIdentityId to the inbound sender." })),
441
+ claimedIdentityId: Type.Optional(Type.String({ description: "Override for the inbound sender's identity. Must match the value used on the request. Leave unset and pass conversationId." })),
369
442
  verificationId: Type.String({ description: "Verification ID returned from request_identity_verification" }),
370
443
  phrase: Type.String({ description: "Three-word phrase the person received via mobile or email" })
371
444
  }),
372
- handler: (params) => client.confirmIdentityVerification({
373
- claimedIdentityId: params.claimedIdentityId,
374
- verificationId: params.verificationId,
375
- phrase: params.phrase
376
- })
445
+ handler: (params) => {
446
+ const resolved = resolveClaimedIdentity(params);
447
+ if ("error" in resolved) return Promise.resolve({ error: resolved.error });
448
+ return client.confirmIdentityVerification({
449
+ claimedIdentityId: resolved.claimedIdentityId,
450
+ verificationId: params.verificationId,
451
+ phrase: params.phrase
452
+ });
453
+ }
377
454
  })
378
455
  ];
379
456
  for (const tool of tools) api.registerTool(tool);
@@ -414,7 +491,9 @@ const plugin = {
414
491
  setCachedResolve(cacheKey, {
415
492
  identityId: r.identityId,
416
493
  status: r.status,
417
- permissions: [...r.permissions, ...forwarded]
494
+ permissions: [...r.permissions, ...forwarded],
495
+ senderProvider: provider,
496
+ senderPlatformId: senderId
418
497
  });
419
498
  if (r.identityId == null) {
420
499
  log.warn(`Identity not provisioned for ${provider}:${senderId} — blocking inbound message`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-identity",
3
- "version": "0.0.19",
3
+ "version": "0.1.0",
4
4
  "description": "OpenClaw identity plugin — identity resolution, access gating, permission enforcement",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin.js",
@@ -29,8 +29,8 @@
29
29
  "dependencies": {
30
30
  "@auriclabs/roles": "0.1.1",
31
31
  "@sinclair/typebox": "^0.34.48",
32
- "@alfe.ai/agent-api-client": "0.1.2",
33
- "@alfe.ai/config": "0.0.8"
32
+ "@alfe.ai/config": "0.0.8",
33
+ "@alfe.ai/agent-api-client": "0.1.2"
34
34
  },
35
35
  "license": "UNLICENSED",
36
36
  "scripts": {