@classytic/arc 2.11.4 → 2.13.1

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 (166) hide show
  1. package/README.md +16 -12
  2. package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
  3. package/dist/EventTransport-CT_52aWU.d.mts +34 -0
  4. package/dist/EventTransport-DLWoUMHy.mjs +103 -0
  5. package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
  6. package/dist/audit/index.d.mts +2 -2
  7. package/dist/audit/index.mjs +1 -1
  8. package/dist/auth/audit.d.mts +199 -0
  9. package/dist/auth/audit.mjs +288 -0
  10. package/dist/auth/index.d.mts +3 -3
  11. package/dist/auth/index.mjs +117 -191
  12. package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
  13. package/dist/buildHandler-olo-gt94.mjs +610 -0
  14. package/dist/cache/index.mjs +3 -3
  15. package/dist/cli/commands/describe.d.mts +89 -13
  16. package/dist/cli/commands/describe.mjs +56 -2
  17. package/dist/cli/commands/docs.mjs +2 -2
  18. package/dist/cli/commands/generate.mjs +147 -48
  19. package/dist/cli/commands/init.d.mts +13 -0
  20. package/dist/cli/commands/init.mjs +130 -87
  21. package/dist/cli/commands/introspect.mjs +8 -1
  22. package/dist/context/index.mjs +1 -1
  23. package/dist/core/index.d.mts +3 -3
  24. package/dist/core/index.mjs +5 -5
  25. package/dist/core-D72ia0EH.mjs +1399 -0
  26. package/dist/{createActionRouter-CIKOcNA7.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
  27. package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
  28. package/dist/{createApp-C9bRrqlX.mjs → createApp-XX2-N0Yd.mjs} +28 -22
  29. package/dist/{defineEvent-D1Ky9M1D.mjs → defineEvent-D5h7EvAx.mjs} +1 -1
  30. package/dist/docs/index.d.mts +1 -1
  31. package/dist/docs/index.mjs +2 -2
  32. package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
  33. package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
  34. package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
  35. package/dist/errors-j4aJm1Wg.mjs +184 -0
  36. package/dist/{eventPlugin-Cts2-Tfj.mjs → eventPlugin-CaKTYkYM.mjs} +28 -4
  37. package/dist/{eventPlugin-DDJoNEPL.d.mts → eventPlugin-qXpqTebY.d.mts} +24 -1
  38. package/dist/events/index.d.mts +6 -6
  39. package/dist/events/index.mjs +11 -35
  40. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  41. package/dist/events/transports/redis.d.mts +1 -1
  42. package/dist/factory/index.d.mts +2 -2
  43. package/dist/factory/index.mjs +2 -2
  44. package/dist/{fields-BRjxOAFp.d.mts → fields-COhcH3fk.d.mts} +23 -2
  45. package/dist/hooks/index.d.mts +1 -1
  46. package/dist/hooks/index.mjs +1 -1
  47. package/dist/idempotency/index.d.mts +1 -1
  48. package/dist/idempotency/index.mjs +1 -20
  49. package/dist/idempotency/redis.mjs +1 -1
  50. package/dist/{index-rHjXmJar.d.mts → index-BTqLEvhu.d.mts} +163 -3
  51. package/dist/{index-CXXRbnf8.d.mts → index-BtW7qYwa.d.mts} +660 -326
  52. package/dist/{index-m8mOOlFW.d.mts → index-Ds61mrJE.d.mts} +50 -4
  53. package/dist/{index-D9t1KNaB.d.mts → index-Dz5IKsrE.d.mts} +360 -219
  54. package/dist/index.d.mts +6 -7
  55. package/dist/index.mjs +9 -10
  56. package/dist/integrations/event-gateway.d.mts +1 -1
  57. package/dist/integrations/event-gateway.mjs +1 -1
  58. package/dist/integrations/index.d.mts +1 -1
  59. package/dist/integrations/mcp/index.d.mts +2 -2
  60. package/dist/integrations/mcp/index.mjs +1 -1
  61. package/dist/integrations/mcp/testing.d.mts +1 -1
  62. package/dist/integrations/mcp/testing.mjs +1 -1
  63. package/dist/integrations/streamline.d.mts +60 -11
  64. package/dist/integrations/streamline.mjs +75 -85
  65. package/dist/integrations/websocket.mjs +2 -8
  66. package/dist/middleware/index.d.mts +1 -1
  67. package/dist/middleware/index.mjs +2 -2
  68. package/dist/migrations/index.d.mts +23 -3
  69. package/dist/migrations/index.mjs +0 -7
  70. package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
  71. package/dist/{openapi-D7G1V7ex.mjs → openapi-CiOMVW1p.mjs} +143 -13
  72. package/dist/org/index.d.mts +2 -2
  73. package/dist/org/index.mjs +1 -1
  74. package/dist/permissions/index.d.mts +3 -3
  75. package/dist/permissions/index.mjs +3 -3
  76. package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
  77. package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
  78. package/dist/pipeline/index.d.mts +1 -1
  79. package/dist/pipeline/index.mjs +1 -1
  80. package/dist/plugins/index.d.mts +16 -31
  81. package/dist/plugins/index.mjs +33 -13
  82. package/dist/plugins/response-cache.mjs +1 -1
  83. package/dist/plugins/tracing-entry.mjs +1 -1
  84. package/dist/presets/filesUpload.d.mts +4 -4
  85. package/dist/presets/filesUpload.mjs +6 -9
  86. package/dist/presets/index.d.mts +1 -1
  87. package/dist/presets/index.mjs +1 -1
  88. package/dist/presets/multiTenant.d.mts +1 -1
  89. package/dist/presets/multiTenant.mjs +2 -2
  90. package/dist/presets/search.d.mts +2 -2
  91. package/dist/presets/search.mjs +6 -8
  92. package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
  93. package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
  94. package/dist/{redis-stream-xTGxB2bm.d.mts → redis-stream-D6HzR1Z_.d.mts} +1 -1
  95. package/dist/registry/index.d.mts +1 -1
  96. package/dist/registry/index.mjs +2 -2
  97. package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
  98. package/dist/{resourceToTools-CxNmI6xF.mjs → resourceToTools-C5coh64w.mjs} +224 -71
  99. package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
  100. package/dist/{schemaIR-Dy2p4MxS.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
  101. package/dist/schemas/index.d.mts +100 -30
  102. package/dist/schemas/index.mjs +86 -29
  103. package/dist/scim/index.d.mts +264 -0
  104. package/dist/scim/index.mjs +963 -0
  105. package/dist/scope/index.d.mts +3 -3
  106. package/dist/scope/index.mjs +4 -4
  107. package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
  108. package/dist/{store-helpers-Cp4uKC1U.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
  109. package/dist/testing/index.d.mts +2 -8
  110. package/dist/testing/index.mjs +16 -24
  111. package/dist/types/index.d.mts +4 -4
  112. package/dist/{types-D7KpfiL1.d.mts → types-BvqwCCSx.d.mts} +73 -25
  113. package/dist/{types-DDyTPc6y.d.mts → types-CTYvcwHe.d.mts} +195 -1
  114. package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
  115. package/dist/{types-BQ9TJQNy.d.mts → types-DQHFc8PM.d.mts} +1 -1
  116. package/dist/utils/index.d.mts +2 -2
  117. package/dist/utils/index.mjs +5 -5
  118. package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
  119. package/dist/{versioning-DsglKfM_.d.mts → versioning-DTTvc80y.d.mts} +1 -1
  120. package/package.json +24 -34
  121. package/skills/arc/SKILL.md +147 -51
  122. package/skills/arc/references/agent-auth.md +238 -0
  123. package/skills/arc/references/api-reference.md +187 -0
  124. package/skills/arc/references/auth.md +354 -7
  125. package/skills/arc/references/enterprise-auth.md +94 -0
  126. package/skills/arc/references/events.md +8 -6
  127. package/skills/arc/references/mcp.md +2 -2
  128. package/skills/arc/references/multi-tenancy.md +11 -2
  129. package/skills/arc/references/production.md +10 -9
  130. package/skills/arc/references/scim.md +247 -0
  131. package/skills/arc/references/testing.md +1 -1
  132. package/skills/arc-code-review/SKILL.md +141 -0
  133. package/skills/arc-code-review/references/anti-patterns.md +911 -0
  134. package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
  135. package/skills/arc-code-review/references/migration-recipes.md +700 -0
  136. package/skills/arc-code-review/references/mongokit-migration.md +386 -0
  137. package/skills/arc-code-review/references/scaffolding.md +230 -0
  138. package/skills/arc-code-review/references/severity.md +127 -0
  139. package/dist/EventTransport-BFQjw9pB.mjs +0 -133
  140. package/dist/EventTransport-CYNUXdCJ.d.mts +0 -293
  141. package/dist/adapters/index.d.mts +0 -3
  142. package/dist/adapters/index.mjs +0 -2
  143. package/dist/adapters-DUUiiimH.mjs +0 -964
  144. package/dist/auth/mongoose.d.mts +0 -191
  145. package/dist/auth/mongoose.mjs +0 -73
  146. package/dist/core-CbcQRIch.mjs +0 -1054
  147. package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
  148. package/dist/errorHandler-DEWmGWPz.d.mts +0 -114
  149. package/dist/errors-D5c-5BJL.mjs +0 -232
  150. package/dist/index-Rg8axYPz.d.mts +0 -370
  151. /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
  152. /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
  153. /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
  154. /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
  155. /package/dist/{elevation-BQQXZ_VR.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
  156. /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
  157. /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
  158. /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
  159. /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
  160. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-DQgqgifU.mjs} +0 -0
  161. /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
  162. /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
  163. /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
  164. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
  165. /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
  166. /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
@@ -0,0 +1,247 @@
1
+ # SCIM 2.0 — IdP Provisioning
2
+
3
+ Arc ships a SCIM 2.0 plugin (`@classytic/arc/scim`) that auto-mounts `/scim/v2/Users` + `/scim/v2/Groups` REST endpoints and translates SCIM wire shapes onto the canonical **repository contract** (`@classytic/repo-core/adapter`). Arc does NOT introduce a SCIM-specific repository subset, controller pipeline, or storage tier — it composes with what kits already expose.
4
+
5
+ ## Honest architecture (what SCIM actually is)
6
+
7
+ **SCIM is a thin REST layer over the kit's `RepositoryLike`.** Whatever plugins you wire at the kit/repo layer (audit, multi-tenant, field-policy, etc.) fire for SCIM exactly the way they fire for arc REST, because **both surfaces call the same repository methods**.
8
+
9
+ ```
10
+ SCIM request → arc/scim plugin → resource.adapter.repository.<method> → kit hooks fire
11
+ ```
12
+
13
+ This is NOT what some earlier docs implied. SCIM does not run arc's HTTP controller pipeline (`auth → permissions → preHandlers → controller → hooks → audit`). SCIM authentication is bearer/verify only at the REST edge; everything else (row-level perms, audit trail, hooks) is composed at the kit layer where it already lives.
14
+
15
+ | Layer | Where it composes | Fires for SCIM? |
16
+ |---|---|---|
17
+ | Bearer / OIDC verify | `scimPlugin({ bearer, verify })` | ✓ at the SCIM edge |
18
+ | Audit trail | `repo.use(auditTrailPlugin())` (mongokit) — kit-specific identifier | ✓ via repo hooks |
19
+ | Multi-tenant scope | `repo.use(multiTenantPlugin)` | ✓ via repo hooks |
20
+ | Field redaction (read) | mongokit's `fieldFilterPlugin` | ✓ on `getAll` / `getById` |
21
+ | Resource hooks | `defineResource({ hooks: { ... } })` | ✗ — those are HTTP-controller hooks; not on the repo path |
22
+ | Per-action permissions | `defineResource({ permissions: { ... } })` | ✗ — same reason; gate at the edge via `verify` |
23
+
24
+ If you want resource hooks to fire for SCIM writes too, install them as **kit plugins** (`repo.on('before:create', fn)`) rather than `defineResource({ hooks: ... })`. The docs for your kit (mongokit / sqlitekit) cover the hook surface.
25
+
26
+ ## CRUD → repository contract mapping
27
+
28
+ | SCIM method | Repository call | Notes |
29
+ |---|---|---|
30
+ | `GET /Users` | `repo.getAll({ filters, page, limit, sort })` | Filter parser maps SCIM filter language → query DSL |
31
+ | `GET /Users/:id` | `repo.getById(id)` | |
32
+ | `POST /Users` | `repo.create(data)` | Inbound SCIM body maps onto resource shape via `mapping.attributes` |
33
+ | `PUT /Users/:id` | `repo.bulkWrite([{ replaceOne: { filter, replacement } }])` | **Kit-conditional** — see "Feature gating" below |
34
+ | `PATCH /Users/:id` | `repo.findOneAndUpdate({ id }, ops)` | Operators flow through unchanged (`$set`, `$unset`, `$push`, `$pull`) |
35
+ | `DELETE /Users/:id` | `repo.delete(id)` | |
36
+
37
+ ## Feature gating (honest about what each kit supports)
38
+
39
+ **SCIM PATCH** translates the RFC 7644 PatchOp body into canonical Mongo-style operators and forwards them to `findOneAndUpdate`. The kit decides what's portable:
40
+
41
+ | Kit | `$set` / scalar overwrite | `$unset` | `$push` / `$pull` (array mutations) |
42
+ |---|---|---|---|
43
+ | **mongokit** | ✓ | ✓ | ✓ — applied natively |
44
+ | **sqlitekit** | ✓ (compiled to flat column writes) | ✓ | ✗ — JSON columns have no array-op semantics; sqlitekit throws cleanly, SCIM 400s with `scimType: invalidValue` |
45
+ | **prismakit** | ✓ | ✓ | depends on column type |
46
+ | **Custom kit (only `MinimalRepo`)** | ✓ via `update(id, $set)` fallback | ✗ — no `findOneAndUpdate` exposed → SCIM 400 | ✗ → SCIM 400 |
47
+
48
+ **SCIM PUT** requires `repo.bulkWrite([{ replaceOne }])` OR a top-level `repo.replace(id, doc)`. Kits that expose neither return **HTTP 501** with a clear message — no silent merge into `update(id, partial)` (which would leave omitted fields surviving and violate SCIM PUT semantics).
49
+
50
+ | Kit | `PUT` (full replace) |
51
+ |---|---|
52
+ | **mongokit** | ✓ via `bulkWrite([{ replaceOne }])` |
53
+ | **sqlitekit** | ✓ via `bulkWrite([{ replaceOne }])` AND `replace(id, doc)` (sqlitekit ≥0.4.0). Routes through `replaceById` (UPDATE with explicit NULLs for omitted columns) so SCIM PUT semantics are honored — omitted fields don't survive. |
54
+ | **prismakit** | varies |
55
+ | **Custom kit (only `MinimalRepo`)** | ✗ — 501 |
56
+
57
+ If your IdP requires PUT semantics and your kit doesn't expose `bulkWrite`, two options: (1) ask the kit team to add it, (2) configure your IdP to use PATCH instead (Okta / Azure AD both support PATCH-only flows).
58
+
59
+ ## Quick start
60
+
61
+ ```typescript
62
+ import { scimPlugin } from "@classytic/arc/scim";
63
+
64
+ await app.register(scimPlugin, {
65
+ users: { resource: userResource }, // your existing arc resource
66
+ groups: { resource: orgResource }, // optional
67
+ bearer: process.env.SCIM_TOKEN, // OR: verify: async (req) => …
68
+ });
69
+ ```
70
+
71
+ That's it. Endpoints mounted: `GET/POST/PUT/PATCH/DELETE /scim/v2/Users[/:id]`, same for `Groups`, plus `ServiceProviderConfig` / `ResourceTypes` / `Schemas` discovery.
72
+
73
+ ## Default mapping (Better Auth aligned)
74
+
75
+ If you don't override `mapping`, SCIM assumes the BA `user` / `organization` schema:
76
+
77
+ | SCIM attribute | Backend field |
78
+ |---|---|
79
+ | `id` | `id` |
80
+ | `userName` | `email` |
81
+ | `name.formatted` / `displayName` | `name` |
82
+ | `emails[].value` | `email` (primary) |
83
+ | `active` | `isActive` |
84
+ | `externalId` | `externalId` |
85
+ | `meta.created` | `createdAt` |
86
+ | `meta.lastModified` | `updatedAt` |
87
+
88
+ For non-BA schemas, override per-attribute:
89
+
90
+ ```typescript
91
+ import { scimPlugin, DEFAULT_USER_MAPPING } from "@classytic/arc/scim";
92
+
93
+ await app.register(scimPlugin, {
94
+ users: {
95
+ resource: userResource,
96
+ mapping: {
97
+ attributes: {
98
+ ...DEFAULT_USER_MAPPING.attributes,
99
+ userName: "username",
100
+ "name.familyName": "lastName",
101
+ },
102
+ },
103
+ },
104
+ bearer: process.env.SCIM_TOKEN,
105
+ });
106
+ ```
107
+
108
+ ## Filter language (RFC 7644 §3.4.2.2)
109
+
110
+ Operators supported: `eq`, `ne`, `co` (contains), `sw` (starts with), `ew` (ends with), `gt`/`ge`/`lt`/`le`, `pr` (present), `and` / `or` / `not`, grouped with `( )`.
111
+
112
+ Real production filters that work out of the box:
113
+
114
+ ```
115
+ filter=userName eq "alice@acme.com" and active eq true
116
+ filter=externalId eq "ad:f3e9-..."
117
+ filter=meta.lastModified gt "2025-01-01T00:00:00Z"
118
+ ```
119
+
120
+ ## Auth — bearer or verify
121
+
122
+ ```typescript
123
+ bearer: process.env.SCIM_TOKEN, // simplest
124
+ // OR
125
+ verify: async (request) => {
126
+ const auth = request.headers.authorization;
127
+ if (!auth?.startsWith("Bearer ")) return false;
128
+ const claims = await verifyJwt(auth.slice(7));
129
+ return claims.scope?.includes("scim:write") ?? false;
130
+ },
131
+ ```
132
+
133
+ Pass exactly one — `bearer` XOR `verify`. The plugin throws at boot if both / neither are configured.
134
+
135
+ ## Observability
136
+
137
+ Every request emits one `ScimObservedEvent`:
138
+
139
+ ```typescript
140
+ {
141
+ resourceType: "Users" | "Groups" | "discovery",
142
+ op: "list" | "get" | "create" | "replace" | "patch" | "delete" | "discovery.<endpoint>",
143
+ status: number,
144
+ durationMs: number,
145
+ scimType?: string, // SCIM error type when failed
146
+ path: string,
147
+ }
148
+ ```
149
+
150
+ Wire to your metrics / logging stack via `observe`:
151
+
152
+ ```typescript
153
+ await app.register(scimPlugin, {
154
+ users: { resource: userResource },
155
+ bearer: process.env.SCIM_TOKEN,
156
+ observe: (event) => {
157
+ metrics.histogram("scim.duration_ms", event.durationMs, {
158
+ op: event.op,
159
+ status: String(event.status),
160
+ });
161
+ },
162
+ });
163
+ ```
164
+
165
+ Default (when `observe` is omitted): `request.log.info({ scim: event }, "scim.request")` — Pino-friendly structured log line.
166
+
167
+ ## Discovery endpoints
168
+
169
+ Auto-mounted; every IdP probes them during connector setup:
170
+
171
+ - `/scim/v2/ServiceProviderConfig` — capability advertisement (`patch.supported: true`, `bulk.supported: false`, `oauthbearertoken` auth)
172
+ - `/scim/v2/ResourceTypes` — `User` (always) + `Group` (when `groups` binding present)
173
+ - `/scim/v2/Schemas` — id + name stub (most IdPs treat as a sanity check)
174
+
175
+ ## Error envelope (RFC 7644 §3.12)
176
+
177
+ Every error response uses the canonical SCIM shape:
178
+
179
+ ```json
180
+ {
181
+ "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
182
+ "status": "400",
183
+ "scimType": "invalidFilter",
184
+ "detail": "Attribute 'xyz' is not filterable"
185
+ }
186
+ ```
187
+
188
+ Content-Type: `application/scim+json` on every response. Parser auto-registered, idempotent on redeclare.
189
+
190
+ ## What's NOT supported (yet)
191
+
192
+ - **Bulk operations** (`/Bulk`) — most IdPs don't use them; discovery advertises `bulk.supported: false`
193
+ - **EnterpriseUser extension** (`employeeNumber`, `manager`, `costCenter`) — pass-through via `mapping.attributes` works today; first-class extension support lands when a paying customer asks
194
+ - **Schema introspection beyond IDs** — `/Schemas` returns id+name only
195
+
196
+ ## Production checklist
197
+
198
+ - [ ] Mount on the same host as your REST surface (no separate SCIM service)
199
+ - [ ] Rotate `SCIM_TOKEN` quarterly; use `verify` callback with short-lived JWTs for multi-IdP setups
200
+ - [ ] Wire your kit's audit plugin at the **kit** layer (mongokit: `repo.use(auditTrailPlugin())` from `@classytic/mongokit/plugins`), not via `defineResource({ audit: true })` — only the kit-layer plugin fires for SCIM writes
201
+ - [ ] Test the Okta / Azure AD connector against `playground/enterprise-auth/` before production cutover
202
+ - [ ] Confirm your kit exposes `findOneAndUpdate` (for PATCH operators) and `bulkWrite` (for PUT) — see "Feature gating" above; otherwise pick the IdP flows your kit supports
203
+
204
+ ## sqlitekit gap message — RESOLVED in sqlitekit ≥0.4.0
205
+
206
+ sqlitekit ≥0.4.0 ships both asks. The original message is preserved below for historical context.
207
+
208
+ **Status of each ask:**
209
+
210
+ 1. **`bulkWrite([{ replaceOne }])` with full-replace semantics — ✓ shipped.** Routes through `actions/update.replaceById` which UPDATE-with-explicit-NULLs every column omitted from the replacement. Pinned by `tests/integration/replace-and-array-ops.test.ts`. The earlier behavior (silent partial-update) is fixed; SCIM PUT clients now see the contract honored.
211
+ 2. **JSON-column array policy — ✓ shipped (option a, refuse cleanly).** `findOneAndUpdate` and `updateMany` throw a clear actionable error on `$push` / `$pull` / `$addToSet` / `$pop` / `$pullAll`, mirroring the refusal `claim()` already shipped. Arc's SCIM plugin translates the throw into `400 Bad Request` with `scimType: invalidValue`.
212
+ 3. **Top-level `replace(id, doc)` — ✓ shipped.** Available alongside `bulkWrite([{ replaceOne }])`; arc can feature-detect either.
213
+
214
+ **Bonus fix:** `findOneAndUpdate` and `updateMany` now also accept raw mongo `$set` / `$unset` / `$inc` / `$setOnInsert` operator records (compiled to flat column writes via the existing `UpdateSpec` path). Previously these silently produced `near "where": syntax error` from Drizzle. SCIM PATCH operator forwarding now works on sqlitekit without an arc-side translation step.
215
+
216
+ ---
217
+
218
+ ### Original gap message (historical)
219
+
220
+ If your sqlitekit-backed app needs SCIM PUT (full replace) or PATCH array ops (`$push`/`$pull`), the underlying repository needs to expose two ops it currently doesn't:
221
+
222
+ > **From:** arc 2.13 SCIM consumer
223
+ >
224
+ > **Subject:** `bulkWrite` + JSON-column array operators for SCIM 2.0 PUT / PATCH
225
+ >
226
+ > **Context.** Arc 2.13 ships a SCIM 2.0 plugin that translates IdP requests onto the canonical `@classytic/repo-core/adapter` `RepositoryLike` contract. PATCH uses `findOneAndUpdate(filter, ops)` so operators (`$set` / `$unset` / `$push` / `$pull`) flow through unchanged. PUT uses `bulkWrite([{ replaceOne }])` because full-document replace isn't in `MinimalRepo`. mongokit covers both today; sqlitekit doesn't.
227
+ >
228
+ > **Asks (in priority order):**
229
+ >
230
+ > 1. **`bulkWrite([...])` with `replaceOne` support.** SCIM PUT (full replace) currently 501s on sqlitekit. The contract: `replaceOne: { filter, replacement }` should DELETE then INSERT (or UPDATE all columns) atomically. Other op types (`updateOne`, `deleteOne`) can stub or fan out to existing methods. Returning `{ matchedCount, modifiedCount }` is enough.
231
+ >
232
+ > 2. **Documented JSON-column array policy for `findOneAndUpdate`.** sqlitekit currently rejects `$push` / `$pull` on `text` (JSON) columns. Three reasonable options for the kit team to pick:
233
+ > - **(a)** Refuse cleanly (current behaviour) — sqlitekit throws; arc 400s with `scimType: invalidValue`. Honest. Document it.
234
+ > - **(b)** Read-modify-write helper (non-atomic). Reads the JSON, mutates, writes back. Document the non-atomicity for hosts to evaluate.
235
+ > - **(c)** SQLite `json_insert` / `json_remove` functions. Atomic via SQL; sqlite-version dependent.
236
+ >
237
+ > arc consumes whatever sqlitekit decides; the SCIM plugin doesn't depend on (b) or (c) being shipped.
238
+ >
239
+ > 3. **Optional: a `replace(id, doc)` top-level convenience.** If the team prefers not to expose `bulkWrite`, a kit-specific `replace(id, doc)` method on `SqliteRepository` would also unlock PUT — arc could feature-detect either path.
240
+ >
241
+ > **No urgency** — SCIM PUT can be substituted with PATCH for most IdPs (Okta / Azure AD support PATCH-only reconciliation modes). This is a "ship when natural" ask, not a blocker for any current consumer.
242
+
243
+ ## See also
244
+
245
+ - [enterprise-auth.md](enterprise-auth.md) — feature matrix
246
+ - [agent-auth.md](agent-auth.md) — DPoP + capability mandates
247
+ - [`playground/enterprise-auth/`](../../../playground/enterprise-auth/) — runnable smoke
@@ -114,7 +114,7 @@ describe('Product resource — full coverage', () => {
114
114
  ## `expectArc(response)` — fluent envelope matchers
115
115
 
116
116
  ```typescript
117
- expectArc(res).ok(); // 200/201, success: true
117
+ expectArc(res).ok(); // 200/201 with data envelope
118
118
  expectArc(res).forbidden(); // 403, arc error envelope
119
119
  expectArc(res).notFound().hasError(/not exist/);
120
120
  expectArc(res).validationError().hasData({ fields: ['email'] });
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: arc-code-review
3
+ description: |
4
+ Audit a client codebase that has @classytic/arc installed for gaps in arc-convention adoption.
5
+ Surfaces hand-rolled CRUD/auth/query/cache code that should be one defineResource() call,
6
+ Mongoose models that should use @classytic/mongokit, manual JSON Schema that should be fieldRules,
7
+ bypassed RequestScope, missing presets, and other patterns that defeat arc's "less code, more
8
+ maintainability" promise. Produces a prioritized migration report with before/after recipes.
9
+ Use when reviewing/auditing a downstream project that depends on @classytic/arc, when the
10
+ user asks for an "arc audit", "arc gap analysis", "arc migration plan", "why isn't arc helping
11
+ us", or when refactoring a Fastify/Express service to arc conventions.
12
+ Triggers: arc audit, arc review, arc gap, arc migration, arc convention check, arc compliance,
13
+ classytic audit, defineResource refactor, mongoose to mongokit, hand-rolled crud to arc,
14
+ arc adoption, arc lint, arc smell, arc anti-pattern.
15
+ license: MIT
16
+ metadata:
17
+ author: Classytic
18
+ tags:
19
+ - arc
20
+ - code-review
21
+ - audit
22
+ - migration
23
+ - refactor
24
+ - fastify
25
+ - mongoose
26
+ - mongokit
27
+ - convention-check
28
+ progressive_disclosure:
29
+ entry_point:
30
+ summary: "Audit a client codebase using @classytic/arc for unrealized convention gains. Detect hand-rolled CRUD/auth/query/cache, Mongoose-without-mongokit, bypassed scope, and emit a prioritized migration report."
31
+ when_to_use: "Reviewing or migrating a project that has arc installed but hasn't adopted defineResource()/presets/permissions/scope/mongokit. Use whenever the user asks 'why isn't arc helping us' or wants a gap analysis."
32
+ quick_start: "1. Confirm arc/mongokit versions 2. Run detection sweep (references/anti-patterns.md) 3. Score each finding (references/severity.md) 4. Emit report using template below"
33
+ context_limit: 900
34
+ ---
35
+
36
+ # arc-code-review
37
+
38
+ Audit skill for client projects that depend on `@classytic/arc`. Detects places where the team is writing code arc would have generated, then emits a prioritized migration plan with concrete before/after diffs.
39
+
40
+ ## When to use
41
+
42
+ Invoke when:
43
+ - The user asks for an "arc audit", "arc gap analysis", "arc migration plan", or "why isn't arc helping us".
44
+ - A repo `dependsOn @classytic/arc` (or `@classytic/mongokit`) and the conversation is about refactoring, code-review, or onboarding.
45
+ - The user mentions hand-rolled CRUD, manual JSON Schema, raw `req.user` access, manual `Model.find()` in route handlers, or hand-written OpenAPI alongside arc.
46
+ - Migrating a Fastify/Express service to arc conventions.
47
+
48
+ **Do NOT use** for arc framework development itself — that's the `arc` skill in `skills/arc/`. This skill audits *consumers* of arc.
49
+
50
+ ## Mental model — what arc replaces
51
+
52
+ One `defineResource()` call **replaces all of these** in a typical Fastify service:
53
+
54
+ | Hand-rolled today | Arc capability that subsumes it | Reference |
55
+ |---|---|---|
56
+ | 5 × `fastify.get/post/patch/delete` per resource | CRUD auto-generation | [arc-cheatsheet.md](references/arc-cheatsheet.md) |
57
+ | `if (req.user.role !== 'admin')` inside handler | `permissions: { create: requireRoles(['admin']) }` | [anti-patterns.md §4](references/anti-patterns.md) |
58
+ | Manual `req.query.filter` parsing, `$or`/`$and` building | `ArcQueryParser` / mongokit `QueryParser` | [anti-patterns.md §1](references/anti-patterns.md) |
59
+ | Hand-written `schema: { body, response }` per route | `schemaOptions.fieldRules` | [anti-patterns.md §2](references/anti-patterns.md) |
60
+ | `schema.set('toJSON', { transform })` to strip `password`/`__v` | `fieldRules: { password: { hidden: true } }` | [anti-patterns.md §5](references/anti-patterns.md) |
61
+ | Hand-maintained `openapi.yaml` / `swagger.json` | `arc docs ./openapi.json` | [anti-patterns.md §6](references/anti-patterns.md) |
62
+ | `eventBus.emit('product.created', ...)` in handler | `events: { created: {} }` (auto-emitted) | [anti-patterns.md §7](references/anti-patterns.md) |
63
+ | `cache.del('products-*')` after mutation | `cache: { tags: ['catalog'] }` (auto-invalidated) | [anti-patterns.md §8](references/anti-patterns.md) |
64
+ | `req.user._id`, `req.user.orgId` direct access | `getUserId(scope)`, `getOrgId(scope)` from `@classytic/arc/scope` | [anti-patterns.md §9](references/anti-patterns.md) |
65
+ | `import mongoose from 'mongoose'` in route/service files | Adapter-only via `createMongooseAdapter` | [anti-patterns.md §10](references/anti-patterns.md) |
66
+ | Soft-delete: `/deleted` route + `deletedAt` field + restore handler | `presets: ['softDelete']` | [anti-patterns.md §14](references/anti-patterns.md) |
67
+ | `class UserRepository { async create() { Model.create() } }` | `new Repository(Model)` (mongokit) | [mongokit-migration.md](references/mongokit-migration.md) |
68
+ | Per-schema `schema.pre('save', ...)` for timestamps/validation | `timestampPlugin()`, `validationChainPlugin()` | [mongokit-migration.md](references/mongokit-migration.md) |
69
+ | Hand-written `name === 'admin'` MCP tool handlers | `mcpPlugin({ resources })` (auto-generated) | [anti-patterns.md §15](references/anti-patterns.md) |
70
+
71
+ ## Audit workflow
72
+
73
+ 1. **Confirm arc is actually installed.** Read root `package.json`. Note `@classytic/arc`, `@classytic/mongokit`, `@classytic/repo-core`, `@classytic/sqlitekit` versions. If arc is absent, this skill doesn't apply — recommend `npx @classytic/arc init` instead.
74
+ 2. **Locate the entry point.** Search for `createApp(` or `defineResource(` to see what arc surface is already in use. Note `auth`, `runtime`, `arcPlugins`, `presets` config.
75
+ 3. **Inventory resources.** For each `defineResource()` call: list `name`, `permissions`, `presets`, `cache`, `schemaOptions`, custom `routes`/`actions`. Compare to what's used.
76
+ 4. **Run the detection sweep.** Walk every section of [anti-patterns.md](references/anti-patterns.md). For each pattern, run the listed grep against `src/` (excluding `node_modules`, `dist`, `test*`). Record file:line of each hit.
77
+ 5. **Score findings.** Apply [severity.md](references/severity.md) rubric (critical / high / medium / low). Critical = security gap or duplicated arc behavior that drifts. Low = cosmetic.
78
+ 6. **Cross-check mongokit adoption.** If `mongoose` is a direct dep but `@classytic/mongokit` is not, every model is a candidate for migration. Use [mongokit-migration.md](references/mongokit-migration.md).
79
+ 7. **Check arc CLI scaffolding hygiene.** Look for `.arcrc`, `arc generate resource` output structure (`{name}.model.ts`, `{name}.repository.ts`, `{name}.resource.ts`). Mismatch = team is hand-creating files. See [scaffolding.md](references/scaffolding.md).
80
+ 8. **Emit the report** using the template below.
81
+
82
+ ## Reporting template
83
+
84
+ ```markdown
85
+ # Arc Convention Audit — <project-name>
86
+
87
+ **Arc version:** 3.0.x · **Mongokit:** <version or "not installed"> · **Sqlitekit:** <version or "n/a"> · **Date:** <YYYY-MM-DD>
88
+ **Files scanned:** <N> · **Findings:** <N critical · <N high · <N medium · <N low>
89
+
90
+ ## Executive summary
91
+ - <1-2 sentences: how much manual code could be deleted, biggest single risk>
92
+ - Estimated LOC removable: ~<N> lines across <N> files
93
+ - Estimated effort: <S/M/L> per resource (<N> resources affected)
94
+
95
+ ## Critical findings
96
+ ### C1. <short title> (<N occurrences>)
97
+ **Pattern:** <what's wrong, in 1 sentence>
98
+ **Locations:** `src/foo/bar.ts:42`, `src/foo/baz.ts:118`, ...
99
+ **Why it matters:** <security / drift / maintainability impact>
100
+ **Fix:** <link to migration recipe>
101
+
102
+ ## High / Medium / Low findings
103
+ (same shape)
104
+
105
+ ## Migration plan (recommended order)
106
+ 1. <Step 1 — usually scope/permissions because they're security-critical>
107
+ 2. <Step 2 — usually CRUD consolidation>
108
+ 3. ...
109
+
110
+ ## Per-resource scorecard
111
+ | Resource | defineResource? | presets used | permissions | cache | events | mongokit | Score |
112
+ |---|---|---|---|---|---|---|---|
113
+ | product | ✅ | softDelete | ✅ | ❌ | manual | ❌ | 6/10 |
114
+ ```
115
+
116
+ ## Severity quick rule
117
+
118
+ - **Critical:** auth bypass, scope leak, fields exposed that should be `hidden`, hand-rolled idempotency that diverges from arc's behavior under load.
119
+ - **High:** duplicated CRUD that will rot (one resource gets a fix the others don't), manual permissions instead of combinators, mongoose imports leaking outside adapters.
120
+ - **Medium:** manual query parsing, manual cache invalidation, missing presets, hand-written OpenAPI.
121
+ - **Low:** style (default exports, `console.log`, `any`), naming conventions, missing `displayName`/`module` metadata.
122
+
123
+ Full rubric → [severity.md](references/severity.md).
124
+
125
+ ## Output discipline
126
+
127
+ - **Cite file:line for every finding.** Do not generalize.
128
+ - **Show the before/after diff** for the first occurrence of each pattern. Subsequent occurrences just list locations.
129
+ - **Quote arc's exact API** in fixes (e.g., `requireRoles(['admin'])` from `@classytic/arc`, not "use arc's role helper").
130
+ - **Sort by severity, then by file count.** A pattern repeated 30× outranks an isolated critical bug for migration ROI.
131
+ - **Distinguish "missing arc feature" from "arc misuse".** The first is opportunity; the second is a bug.
132
+ - **Don't recommend arc features the project hasn't enabled.** If `arcPlugins.queryCache: false`, don't suggest cache tags — recommend enabling it first.
133
+
134
+ ## References
135
+
136
+ - **[anti-patterns.md](references/anti-patterns.md)** — every greppable anti-pattern with detection regex, severity, and fix
137
+ - **[migration-recipes.md](references/migration-recipes.md)** — concrete before/after diffs (manual CRUD → defineResource, manual perms → combinators, manual events, etc.)
138
+ - **[mongokit-migration.md](references/mongokit-migration.md)** — Mongoose-only project → mongokit Repository + plugins
139
+ - **[arc-cheatsheet.md](references/arc-cheatsheet.md)** — what arc provides at a glance (defineResource fields, presets, permissions, scope, hooks, events, cache, MCP)
140
+ - **[scaffolding.md](references/scaffolding.md)** — arc CLI (`init`, `generate resource`, `docs`, `introspect`, `doctor`), `.arcrc`, file conventions
141
+ - **[severity.md](references/severity.md)** — severity rubric and triage examples