@shipfox/api-agent-access 20.3.0 → 20.4.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.
@@ -1,2 +1,2 @@
1
1
  $ shipfox-swc
2
- Successfully compiled: 14 files with swc (323.19ms)
2
+ Successfully compiled: 14 files with swc (348.69ms)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @shipfox/api-agent-access
2
2
 
3
+ ## 20.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0b32d1a: Remove personal access token support from agent access.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [9a66057]
12
+ - Updated dependencies [0b32d1a]
13
+ - @shipfox/api-workflows-dto@20.4.0
14
+ - @shipfox/api-auth-context@20.4.0
15
+
3
16
  ## 20.3.0
4
17
 
5
18
  ### Minor Changes
@@ -61,7 +61,7 @@ import { AGENT_ACCESS_TOOL_CALL_LIMIT, AGENT_ACCESS_TOOL_CALL_WINDOW_MS } from '
61
61
  }
62
62
  }
63
63
  function credentialKey(credential) {
64
- return credential.kind === 'oauth_grant' ? `oauth_grant:${credential.grantId}` : `pat:${credential.patId}`;
64
+ return `oauth_grant:${credential.grantId}`;
65
65
  }
66
66
 
67
67
  //# sourceMappingURL=rate-limiter.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/rate-limiter.ts"],"sourcesContent":["import type {AgentAccessCredential} from '@shipfox/api-auth-context';\nimport {AGENT_ACCESS_TOOL_CALL_LIMIT, AGENT_ACCESS_TOOL_CALL_WINDOW_MS} from '#constants.js';\n\nexport interface AgentAccessRateLimitDecision {\n allowed: boolean;\n retry_after_seconds?: number | undefined;\n}\n\nexport interface AgentAccessRateLimiter {\n consume(credential: AgentAccessCredential): AgentAccessRateLimitDecision;\n check(credential: AgentAccessCredential): AgentAccessRateLimitDecision;\n size(): number;\n}\n\nexport interface CreateAgentAccessRateLimiterOptions {\n now?: () => number;\n limit?: number;\n windowMs?: number;\n}\n\ninterface Bucket {\n startedAt: number;\n count: number;\n}\n\n/** A flat, process-local fixed window keyed by the authenticated credential. */\nexport function createAgentAccessRateLimiter(\n options: CreateAgentAccessRateLimiterOptions = {},\n): AgentAccessRateLimiter {\n const now = options.now ?? Date.now;\n const limit = options.limit ?? AGENT_ACCESS_TOOL_CALL_LIMIT;\n const windowMs = options.windowMs ?? AGENT_ACCESS_TOOL_CALL_WINDOW_MS;\n if (!Number.isInteger(limit) || limit < 1) throw new Error('Agent-access rate limit is invalid');\n if (!Number.isInteger(windowMs) || windowMs < 1) {\n throw new Error('Agent-access rate-limit window is invalid');\n }\n\n const buckets = new Map<string, Bucket>();\n\n const consume = (credential: AgentAccessCredential): AgentAccessRateLimitDecision => {\n const timestamp = now();\n pruneExpiredBuckets(timestamp);\n const key = credentialKey(credential);\n const bucket = activeBucket(key, timestamp) ?? {startedAt: timestamp, count: 0};\n\n const decision = decisionForBucket(bucket, timestamp);\n if (!decision.allowed) return decision;\n\n bucket.count += 1;\n buckets.set(key, bucket);\n return {allowed: true};\n };\n\n const check = (credential: AgentAccessCredential): AgentAccessRateLimitDecision => {\n const timestamp = now();\n pruneExpiredBuckets(timestamp);\n const bucket = activeBucket(credentialKey(credential), timestamp);\n return bucket === undefined ? {allowed: true} : decisionForBucket(bucket, timestamp);\n };\n\n return {\n consume,\n check,\n size: () => buckets.size,\n };\n\n function activeBucket(key: string, timestamp: number): Bucket | undefined {\n const bucket = buckets.get(key);\n if (bucket !== undefined && timestamp >= bucket.startedAt + windowMs) {\n buckets.delete(key);\n return undefined;\n }\n return bucket;\n }\n\n function decisionForBucket(bucket: Bucket, timestamp: number): AgentAccessRateLimitDecision {\n if (bucket.count < limit) return {allowed: true};\n return {\n allowed: false,\n retry_after_seconds: Math.max(1, Math.ceil((bucket.startedAt + windowMs - timestamp) / 1000)),\n };\n }\n\n function pruneExpiredBuckets(timestamp: number): void {\n for (const [key, bucket] of buckets) {\n if (timestamp >= bucket.startedAt + windowMs) buckets.delete(key);\n }\n }\n}\n\nfunction credentialKey(credential: AgentAccessCredential): string {\n return credential.kind === 'oauth_grant'\n ? `oauth_grant:${credential.grantId}`\n : `pat:${credential.patId}`;\n}\n"],"names":["AGENT_ACCESS_TOOL_CALL_LIMIT","AGENT_ACCESS_TOOL_CALL_WINDOW_MS","createAgentAccessRateLimiter","options","now","Date","limit","windowMs","Number","isInteger","Error","buckets","Map","consume","credential","timestamp","pruneExpiredBuckets","key","credentialKey","bucket","activeBucket","startedAt","count","decision","decisionForBucket","allowed","set","check","undefined","size","get","delete","retry_after_seconds","Math","max","ceil","kind","grantId","patId"],"mappings":"AACA,SAAQA,4BAA4B,EAAEC,gCAAgC,QAAO,gBAAgB;AAwB7F,8EAA8E,GAC9E,OAAO,SAASC,6BACdC,UAA+C,CAAC,CAAC;IAEjD,MAAMC,MAAMD,QAAQC,GAAG,IAAIC,KAAKD,GAAG;IACnC,MAAME,QAAQH,QAAQG,KAAK,IAAIN;IAC/B,MAAMO,WAAWJ,QAAQI,QAAQ,IAAIN;IACrC,IAAI,CAACO,OAAOC,SAAS,CAACH,UAAUA,QAAQ,GAAG,MAAM,IAAII,MAAM;IAC3D,IAAI,CAACF,OAAOC,SAAS,CAACF,aAAaA,WAAW,GAAG;QAC/C,MAAM,IAAIG,MAAM;IAClB;IAEA,MAAMC,UAAU,IAAIC;IAEpB,MAAMC,UAAU,CAACC;QACf,MAAMC,YAAYX;QAClBY,oBAAoBD;QACpB,MAAME,MAAMC,cAAcJ;QAC1B,MAAMK,SAASC,aAAaH,KAAKF,cAAc;YAACM,WAAWN;YAAWO,OAAO;QAAC;QAE9E,MAAMC,WAAWC,kBAAkBL,QAAQJ;QAC3C,IAAI,CAACQ,SAASE,OAAO,EAAE,OAAOF;QAE9BJ,OAAOG,KAAK,IAAI;QAChBX,QAAQe,GAAG,CAACT,KAAKE;QACjB,OAAO;YAACM,SAAS;QAAI;IACvB;IAEA,MAAME,QAAQ,CAACb;QACb,MAAMC,YAAYX;QAClBY,oBAAoBD;QACpB,MAAMI,SAASC,aAAaF,cAAcJ,aAAaC;QACvD,OAAOI,WAAWS,YAAY;YAACH,SAAS;QAAI,IAAID,kBAAkBL,QAAQJ;IAC5E;IAEA,OAAO;QACLF;QACAc;QACAE,MAAM,IAAMlB,QAAQkB,IAAI;IAC1B;IAEA,SAAST,aAAaH,GAAW,EAAEF,SAAiB;QAClD,MAAMI,SAASR,QAAQmB,GAAG,CAACb;QAC3B,IAAIE,WAAWS,aAAab,aAAaI,OAAOE,SAAS,GAAGd,UAAU;YACpEI,QAAQoB,MAAM,CAACd;YACf,OAAOW;QACT;QACA,OAAOT;IACT;IAEA,SAASK,kBAAkBL,MAAc,EAAEJ,SAAiB;QAC1D,IAAII,OAAOG,KAAK,GAAGhB,OAAO,OAAO;YAACmB,SAAS;QAAI;QAC/C,OAAO;YACLA,SAAS;YACTO,qBAAqBC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,IAAI,CAAC,AAAChB,CAAAA,OAAOE,SAAS,GAAGd,WAAWQ,SAAQ,IAAK;QACzF;IACF;IAEA,SAASC,oBAAoBD,SAAiB;QAC5C,KAAK,MAAM,CAACE,KAAKE,OAAO,IAAIR,QAAS;YACnC,IAAII,aAAaI,OAAOE,SAAS,GAAGd,UAAUI,QAAQoB,MAAM,CAACd;QAC/D;IACF;AACF;AAEA,SAASC,cAAcJ,UAAiC;IACtD,OAAOA,WAAWsB,IAAI,KAAK,gBACvB,CAAC,YAAY,EAAEtB,WAAWuB,OAAO,EAAE,GACnC,CAAC,IAAI,EAAEvB,WAAWwB,KAAK,EAAE;AAC/B"}
1
+ {"version":3,"sources":["../../src/core/rate-limiter.ts"],"sourcesContent":["import type {AgentAccessCredential} from '@shipfox/api-auth-context';\nimport {AGENT_ACCESS_TOOL_CALL_LIMIT, AGENT_ACCESS_TOOL_CALL_WINDOW_MS} from '#constants.js';\n\nexport interface AgentAccessRateLimitDecision {\n allowed: boolean;\n retry_after_seconds?: number | undefined;\n}\n\nexport interface AgentAccessRateLimiter {\n consume(credential: AgentAccessCredential): AgentAccessRateLimitDecision;\n check(credential: AgentAccessCredential): AgentAccessRateLimitDecision;\n size(): number;\n}\n\nexport interface CreateAgentAccessRateLimiterOptions {\n now?: () => number;\n limit?: number;\n windowMs?: number;\n}\n\ninterface Bucket {\n startedAt: number;\n count: number;\n}\n\n/** A flat, process-local fixed window keyed by the authenticated credential. */\nexport function createAgentAccessRateLimiter(\n options: CreateAgentAccessRateLimiterOptions = {},\n): AgentAccessRateLimiter {\n const now = options.now ?? Date.now;\n const limit = options.limit ?? AGENT_ACCESS_TOOL_CALL_LIMIT;\n const windowMs = options.windowMs ?? AGENT_ACCESS_TOOL_CALL_WINDOW_MS;\n if (!Number.isInteger(limit) || limit < 1) throw new Error('Agent-access rate limit is invalid');\n if (!Number.isInteger(windowMs) || windowMs < 1) {\n throw new Error('Agent-access rate-limit window is invalid');\n }\n\n const buckets = new Map<string, Bucket>();\n\n const consume = (credential: AgentAccessCredential): AgentAccessRateLimitDecision => {\n const timestamp = now();\n pruneExpiredBuckets(timestamp);\n const key = credentialKey(credential);\n const bucket = activeBucket(key, timestamp) ?? {startedAt: timestamp, count: 0};\n\n const decision = decisionForBucket(bucket, timestamp);\n if (!decision.allowed) return decision;\n\n bucket.count += 1;\n buckets.set(key, bucket);\n return {allowed: true};\n };\n\n const check = (credential: AgentAccessCredential): AgentAccessRateLimitDecision => {\n const timestamp = now();\n pruneExpiredBuckets(timestamp);\n const bucket = activeBucket(credentialKey(credential), timestamp);\n return bucket === undefined ? {allowed: true} : decisionForBucket(bucket, timestamp);\n };\n\n return {\n consume,\n check,\n size: () => buckets.size,\n };\n\n function activeBucket(key: string, timestamp: number): Bucket | undefined {\n const bucket = buckets.get(key);\n if (bucket !== undefined && timestamp >= bucket.startedAt + windowMs) {\n buckets.delete(key);\n return undefined;\n }\n return bucket;\n }\n\n function decisionForBucket(bucket: Bucket, timestamp: number): AgentAccessRateLimitDecision {\n if (bucket.count < limit) return {allowed: true};\n return {\n allowed: false,\n retry_after_seconds: Math.max(1, Math.ceil((bucket.startedAt + windowMs - timestamp) / 1000)),\n };\n }\n\n function pruneExpiredBuckets(timestamp: number): void {\n for (const [key, bucket] of buckets) {\n if (timestamp >= bucket.startedAt + windowMs) buckets.delete(key);\n }\n }\n}\n\nfunction credentialKey(credential: AgentAccessCredential): string {\n return `oauth_grant:${credential.grantId}`;\n}\n"],"names":["AGENT_ACCESS_TOOL_CALL_LIMIT","AGENT_ACCESS_TOOL_CALL_WINDOW_MS","createAgentAccessRateLimiter","options","now","Date","limit","windowMs","Number","isInteger","Error","buckets","Map","consume","credential","timestamp","pruneExpiredBuckets","key","credentialKey","bucket","activeBucket","startedAt","count","decision","decisionForBucket","allowed","set","check","undefined","size","get","delete","retry_after_seconds","Math","max","ceil","grantId"],"mappings":"AACA,SAAQA,4BAA4B,EAAEC,gCAAgC,QAAO,gBAAgB;AAwB7F,8EAA8E,GAC9E,OAAO,SAASC,6BACdC,UAA+C,CAAC,CAAC;IAEjD,MAAMC,MAAMD,QAAQC,GAAG,IAAIC,KAAKD,GAAG;IACnC,MAAME,QAAQH,QAAQG,KAAK,IAAIN;IAC/B,MAAMO,WAAWJ,QAAQI,QAAQ,IAAIN;IACrC,IAAI,CAACO,OAAOC,SAAS,CAACH,UAAUA,QAAQ,GAAG,MAAM,IAAII,MAAM;IAC3D,IAAI,CAACF,OAAOC,SAAS,CAACF,aAAaA,WAAW,GAAG;QAC/C,MAAM,IAAIG,MAAM;IAClB;IAEA,MAAMC,UAAU,IAAIC;IAEpB,MAAMC,UAAU,CAACC;QACf,MAAMC,YAAYX;QAClBY,oBAAoBD;QACpB,MAAME,MAAMC,cAAcJ;QAC1B,MAAMK,SAASC,aAAaH,KAAKF,cAAc;YAACM,WAAWN;YAAWO,OAAO;QAAC;QAE9E,MAAMC,WAAWC,kBAAkBL,QAAQJ;QAC3C,IAAI,CAACQ,SAASE,OAAO,EAAE,OAAOF;QAE9BJ,OAAOG,KAAK,IAAI;QAChBX,QAAQe,GAAG,CAACT,KAAKE;QACjB,OAAO;YAACM,SAAS;QAAI;IACvB;IAEA,MAAME,QAAQ,CAACb;QACb,MAAMC,YAAYX;QAClBY,oBAAoBD;QACpB,MAAMI,SAASC,aAAaF,cAAcJ,aAAaC;QACvD,OAAOI,WAAWS,YAAY;YAACH,SAAS;QAAI,IAAID,kBAAkBL,QAAQJ;IAC5E;IAEA,OAAO;QACLF;QACAc;QACAE,MAAM,IAAMlB,QAAQkB,IAAI;IAC1B;IAEA,SAAST,aAAaH,GAAW,EAAEF,SAAiB;QAClD,MAAMI,SAASR,QAAQmB,GAAG,CAACb;QAC3B,IAAIE,WAAWS,aAAab,aAAaI,OAAOE,SAAS,GAAGd,UAAU;YACpEI,QAAQoB,MAAM,CAACd;YACf,OAAOW;QACT;QACA,OAAOT;IACT;IAEA,SAASK,kBAAkBL,MAAc,EAAEJ,SAAiB;QAC1D,IAAII,OAAOG,KAAK,GAAGhB,OAAO,OAAO;YAACmB,SAAS;QAAI;QAC/C,OAAO;YACLA,SAAS;YACTO,qBAAqBC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,IAAI,CAAC,AAAChB,CAAAA,OAAOE,SAAS,GAAGd,WAAWQ,SAAQ,IAAK;QACzF;IACF;IAEA,SAASC,oBAAoBD,SAAiB;QAC5C,KAAK,MAAM,CAACE,KAAKE,OAAO,IAAIR,QAAS;YACnC,IAAII,aAAaI,OAAOE,SAAS,GAAGd,UAAUI,QAAQoB,MAAM,CAACd;QAC/D;IACF;AACF;AAEA,SAASC,cAAcJ,UAAiC;IACtD,OAAO,CAAC,YAAY,EAAEA,WAAWsB,OAAO,EAAE;AAC5C"}
@@ -20,8 +20,8 @@ function auditLogContext(record) {
20
20
  userId: record.context.userId,
21
21
  workspaceId: record.context.workspaceId,
22
22
  credentialKind: credential.kind,
23
- credentialId: credential.kind === 'oauth_grant' ? credential.grantId : credential.patId,
24
- clientId: credential.kind === 'oauth_grant' ? credential.clientId : null
23
+ credentialId: credential.grantId,
24
+ clientId: credential.clientId
25
25
  };
26
26
  }
27
27
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/presentation/audit.ts"],"sourcesContent":["import type {AgentAccessContext} from '@shipfox/api-auth-context';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {type AgentAccessToolCallOutcome, recordAgentAccessToolCall} from '#metrics/index.js';\n\nexport interface AgentAccessToolCallAuditRecord {\n tool: string;\n outcome: AgentAccessToolCallOutcome;\n errorCode: string;\n context: AgentAccessContext;\n}\n\nexport type AgentAccessToolCallRecorder = (record: AgentAccessToolCallAuditRecord) => void;\n\nexport interface CreateAgentAccessToolCallRecorderOptions {\n recordMetric?: typeof recordAgentAccessToolCall;\n logInfo?:\n | ((context: Record<string, unknown>, message: 'agent access tool call audited') => void)\n | undefined;\n}\n\nexport function createAgentAccessToolCallRecorder(\n options: CreateAgentAccessToolCallRecorderOptions = {},\n): AgentAccessToolCallRecorder {\n const recordMetric = options.recordMetric ?? recordAgentAccessToolCall;\n const logInfo = options.logInfo ?? ((context, message) => logger().info(context, message));\n\n return (record) => {\n recordMetric({tool: record.tool, outcome: record.outcome});\n logInfo(auditLogContext(record), 'agent access tool call audited');\n };\n}\n\nfunction auditLogContext(record: AgentAccessToolCallAuditRecord): Record<string, unknown> {\n const credential = record.context.credential;\n return {\n tool: record.tool,\n outcome: record.outcome,\n errorCode: record.errorCode,\n userId: record.context.userId,\n workspaceId: record.context.workspaceId,\n credentialKind: credential.kind,\n credentialId: credential.kind === 'oauth_grant' ? credential.grantId : credential.patId,\n clientId: credential.kind === 'oauth_grant' ? credential.clientId : null,\n };\n}\n"],"names":["logger","recordAgentAccessToolCall","createAgentAccessToolCallRecorder","options","recordMetric","logInfo","context","message","info","record","tool","outcome","auditLogContext","credential","errorCode","userId","workspaceId","credentialKind","kind","credentialId","grantId","patId","clientId"],"mappings":"AACA,SAAQA,MAAM,QAAO,8BAA8B;AACnD,SAAyCC,yBAAyB,QAAO,oBAAoB;AAkB7F,OAAO,SAASC,kCACdC,UAAoD,CAAC,CAAC;IAEtD,MAAMC,eAAeD,QAAQC,YAAY,IAAIH;IAC7C,MAAMI,UAAUF,QAAQE,OAAO,IAAK,CAAA,CAACC,SAASC,UAAYP,SAASQ,IAAI,CAACF,SAASC,QAAO;IAExF,OAAO,CAACE;QACNL,aAAa;YAACM,MAAMD,OAAOC,IAAI;YAAEC,SAASF,OAAOE,OAAO;QAAA;QACxDN,QAAQO,gBAAgBH,SAAS;IACnC;AACF;AAEA,SAASG,gBAAgBH,MAAsC;IAC7D,MAAMI,aAAaJ,OAAOH,OAAO,CAACO,UAAU;IAC5C,OAAO;QACLH,MAAMD,OAAOC,IAAI;QACjBC,SAASF,OAAOE,OAAO;QACvBG,WAAWL,OAAOK,SAAS;QAC3BC,QAAQN,OAAOH,OAAO,CAACS,MAAM;QAC7BC,aAAaP,OAAOH,OAAO,CAACU,WAAW;QACvCC,gBAAgBJ,WAAWK,IAAI;QAC/BC,cAAcN,WAAWK,IAAI,KAAK,gBAAgBL,WAAWO,OAAO,GAAGP,WAAWQ,KAAK;QACvFC,UAAUT,WAAWK,IAAI,KAAK,gBAAgBL,WAAWS,QAAQ,GAAG;IACtE;AACF"}
1
+ {"version":3,"sources":["../../src/presentation/audit.ts"],"sourcesContent":["import type {AgentAccessContext} from '@shipfox/api-auth-context';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {type AgentAccessToolCallOutcome, recordAgentAccessToolCall} from '#metrics/index.js';\n\nexport interface AgentAccessToolCallAuditRecord {\n tool: string;\n outcome: AgentAccessToolCallOutcome;\n errorCode: string;\n context: AgentAccessContext;\n}\n\nexport type AgentAccessToolCallRecorder = (record: AgentAccessToolCallAuditRecord) => void;\n\nexport interface CreateAgentAccessToolCallRecorderOptions {\n recordMetric?: typeof recordAgentAccessToolCall;\n logInfo?:\n | ((context: Record<string, unknown>, message: 'agent access tool call audited') => void)\n | undefined;\n}\n\nexport function createAgentAccessToolCallRecorder(\n options: CreateAgentAccessToolCallRecorderOptions = {},\n): AgentAccessToolCallRecorder {\n const recordMetric = options.recordMetric ?? recordAgentAccessToolCall;\n const logInfo = options.logInfo ?? ((context, message) => logger().info(context, message));\n\n return (record) => {\n recordMetric({tool: record.tool, outcome: record.outcome});\n logInfo(auditLogContext(record), 'agent access tool call audited');\n };\n}\n\nfunction auditLogContext(record: AgentAccessToolCallAuditRecord): Record<string, unknown> {\n const credential = record.context.credential;\n return {\n tool: record.tool,\n outcome: record.outcome,\n errorCode: record.errorCode,\n userId: record.context.userId,\n workspaceId: record.context.workspaceId,\n credentialKind: credential.kind,\n credentialId: credential.grantId,\n clientId: credential.clientId,\n };\n}\n"],"names":["logger","recordAgentAccessToolCall","createAgentAccessToolCallRecorder","options","recordMetric","logInfo","context","message","info","record","tool","outcome","auditLogContext","credential","errorCode","userId","workspaceId","credentialKind","kind","credentialId","grantId","clientId"],"mappings":"AACA,SAAQA,MAAM,QAAO,8BAA8B;AACnD,SAAyCC,yBAAyB,QAAO,oBAAoB;AAkB7F,OAAO,SAASC,kCACdC,UAAoD,CAAC,CAAC;IAEtD,MAAMC,eAAeD,QAAQC,YAAY,IAAIH;IAC7C,MAAMI,UAAUF,QAAQE,OAAO,IAAK,CAAA,CAACC,SAASC,UAAYP,SAASQ,IAAI,CAACF,SAASC,QAAO;IAExF,OAAO,CAACE;QACNL,aAAa;YAACM,MAAMD,OAAOC,IAAI;YAAEC,SAASF,OAAOE,OAAO;QAAA;QACxDN,QAAQO,gBAAgBH,SAAS;IACnC;AACF;AAEA,SAASG,gBAAgBH,MAAsC;IAC7D,MAAMI,aAAaJ,OAAOH,OAAO,CAACO,UAAU;IAC5C,OAAO;QACLH,MAAMD,OAAOC,IAAI;QACjBC,SAASF,OAAOE,OAAO;QACvBG,WAAWL,OAAOK,SAAS;QAC3BC,QAAQN,OAAOH,OAAO,CAACS,MAAM;QAC7BC,aAAaP,OAAOH,OAAO,CAACU,WAAW;QACvCC,gBAAgBJ,WAAWK,IAAI;QAC/BC,cAAcN,WAAWO,OAAO;QAChCC,UAAUR,WAAWQ,QAAQ;IAC/B;AACF"}