@lupinum/better-convex-nuxt 0.8.0-beta.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.
Files changed (268) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSES/Apache-2.0.txt +201 -0
  3. package/README.md +151 -0
  4. package/THIRD_PARTY_NOTICES.md +38 -0
  5. package/dist/module.d.mts +55 -0
  6. package/dist/module.json +12 -0
  7. package/dist/module.mjs +309 -0
  8. package/dist/runtime/auth/auth-identity.d.ts +31 -0
  9. package/dist/runtime/auth/auth-identity.js +21 -0
  10. package/dist/runtime/auth/better-auth-browser-adapter.d.ts +33 -0
  11. package/dist/runtime/auth/better-auth-browser-adapter.js +215 -0
  12. package/dist/runtime/auth/client-engine-types.d.ts +4 -0
  13. package/dist/runtime/auth/client-engine-types.js +0 -0
  14. package/dist/runtime/auth/integrated-client.d.ts +25 -0
  15. package/dist/runtime/auth/integrated-client.js +132 -0
  16. package/dist/runtime/auth/operation-tracker.d.ts +7 -0
  17. package/dist/runtime/auth/operation-tracker.js +14 -0
  18. package/dist/runtime/auth/session-synchronization.d.ts +23 -0
  19. package/dist/runtime/auth/session-synchronization.js +156 -0
  20. package/dist/runtime/auth/token-fetcher.d.ts +66 -0
  21. package/dist/runtime/auth/token-fetcher.js +131 -0
  22. package/dist/runtime/auth/validate-auth-client-definition.d.ts +12 -0
  23. package/dist/runtime/auth/validate-auth-client-definition.js +47 -0
  24. package/dist/runtime/auth-client/convex-client-plugin.d.ts +31 -0
  25. package/dist/runtime/auth-client/convex-client-plugin.js +14 -0
  26. package/dist/runtime/auth-client/definition-types.d.ts +6 -0
  27. package/dist/runtime/auth-client/definition-types.js +0 -0
  28. package/dist/runtime/auth-client/empty-definition.d.ts +2 -0
  29. package/dist/runtime/auth-client/empty-definition.js +2 -0
  30. package/dist/runtime/auth-client/index.d.ts +71 -0
  31. package/dist/runtime/auth-client/index.js +3 -0
  32. package/dist/runtime/cli/auth-schema.d.ts +2 -0
  33. package/dist/runtime/cli/auth-schema.js +169 -0
  34. package/dist/runtime/cli/convex.d.ts +11 -0
  35. package/dist/runtime/cli/convex.js +274 -0
  36. package/dist/runtime/composables/useConvex.d.ts +4 -0
  37. package/dist/runtime/composables/useConvex.js +9 -0
  38. package/dist/runtime/composables/useConvexAction.d.ts +4 -0
  39. package/dist/runtime/composables/useConvexAction.js +18 -0
  40. package/dist/runtime/composables/useConvexAttachment.d.ts +9 -0
  41. package/dist/runtime/composables/useConvexAttachment.js +14 -0
  42. package/dist/runtime/composables/useConvexAuth.d.ts +10 -0
  43. package/dist/runtime/composables/useConvexAuth.js +37 -0
  44. package/dist/runtime/composables/useConvexConfig.d.ts +12 -0
  45. package/dist/runtime/composables/useConvexConfig.js +7 -0
  46. package/dist/runtime/composables/useConvexConnectionState.d.ts +33 -0
  47. package/dist/runtime/composables/useConvexConnectionState.js +24 -0
  48. package/dist/runtime/composables/useConvexFileUpload.d.ts +169 -0
  49. package/dist/runtime/composables/useConvexFileUpload.js +198 -0
  50. package/dist/runtime/composables/useConvexMutation.d.ts +13 -0
  51. package/dist/runtime/composables/useConvexMutation.js +24 -0
  52. package/dist/runtime/composables/useConvexPaginatedQuery.d.ts +13 -0
  53. package/dist/runtime/composables/useConvexPaginatedQuery.js +256 -0
  54. package/dist/runtime/composables/useConvexQuery.d.ts +19 -0
  55. package/dist/runtime/composables/useConvexQuery.js +308 -0
  56. package/dist/runtime/convex-auth/adapter/create-adapter.d.ts +33 -0
  57. package/dist/runtime/convex-auth/adapter/create-adapter.js +308 -0
  58. package/dist/runtime/convex-auth/adapter/define-functions.d.ts +160 -0
  59. package/dist/runtime/convex-auth/adapter/define-functions.js +497 -0
  60. package/dist/runtime/convex-auth/adapter/generate-schema.d.ts +16 -0
  61. package/dist/runtime/convex-auth/adapter/generate-schema.js +332 -0
  62. package/dist/runtime/convex-auth/adapter/metadata.d.ts +44 -0
  63. package/dist/runtime/convex-auth/adapter/metadata.js +91 -0
  64. package/dist/runtime/convex-auth/adapter/query.d.ts +32 -0
  65. package/dist/runtime/convex-auth/adapter/query.js +275 -0
  66. package/dist/runtime/convex-auth/adapter/relationships.d.ts +20 -0
  67. package/dist/runtime/convex-auth/adapter/relationships.js +128 -0
  68. package/dist/runtime/convex-auth/component/_generated/api.js +5 -0
  69. package/dist/runtime/convex-auth/component/_generated/component.d.ts +116 -0
  70. package/dist/runtime/convex-auth/component/_generated/component.js +0 -0
  71. package/dist/runtime/convex-auth/component/_generated/dataModel.js +0 -0
  72. package/dist/runtime/convex-auth/component/_generated/server.js +16 -0
  73. package/dist/runtime/convex-auth/component/adapter.d.ts +142 -0
  74. package/dist/runtime/convex-auth/component/adapter.js +16 -0
  75. package/dist/runtime/convex-auth/component/convex.config.d.ts +2 -0
  76. package/dist/runtime/convex-auth/component/convex.config.js +3 -0
  77. package/dist/runtime/convex-auth/component/schema.d.ts +799 -0
  78. package/dist/runtime/convex-auth/component/schema.js +180 -0
  79. package/dist/runtime/convex-auth/component/schemaMetadata.d.ts +2094 -0
  80. package/dist/runtime/convex-auth/component/schemaMetadata.js +2161 -0
  81. package/dist/runtime/convex-auth/context.d.ts +5 -0
  82. package/dist/runtime/convex-auth/context.js +8 -0
  83. package/dist/runtime/convex-auth/create-auth-component.d.ts +41 -0
  84. package/dist/runtime/convex-auth/create-auth-component.js +157 -0
  85. package/dist/runtime/convex-auth/index.d.ts +11 -0
  86. package/dist/runtime/convex-auth/index.js +7 -0
  87. package/dist/runtime/convex-auth/internal-session.d.ts +2 -0
  88. package/dist/runtime/convex-auth/internal-session.js +1 -0
  89. package/dist/runtime/convex-auth/jwks-rotation.d.ts +58 -0
  90. package/dist/runtime/convex-auth/jwks-rotation.js +163 -0
  91. package/dist/runtime/convex-auth/oauth-live-access.d.ts +17 -0
  92. package/dist/runtime/convex-auth/oauth-live-access.js +78 -0
  93. package/dist/runtime/convex-auth/oauth-provider-compat.d.ts +10 -0
  94. package/dist/runtime/convex-auth/oauth-provider-compat.js +170 -0
  95. package/dist/runtime/convex-auth/oauth-resource.d.ts +39 -0
  96. package/dist/runtime/convex-auth/oauth-resource.js +118 -0
  97. package/dist/runtime/convex-auth/oauth-security.d.ts +82 -0
  98. package/dist/runtime/convex-auth/oauth-security.js +454 -0
  99. package/dist/runtime/convex-auth/origin.d.ts +4 -0
  100. package/dist/runtime/convex-auth/origin.js +9 -0
  101. package/dist/runtime/convex-auth/plugin.d.ts +18 -0
  102. package/dist/runtime/convex-auth/plugin.js +605 -0
  103. package/dist/runtime/convex-auth/provider.d.ts +2 -0
  104. package/dist/runtime/convex-auth/provider.js +11 -0
  105. package/dist/runtime/convex-auth/test.d.ts +410 -0
  106. package/dist/runtime/convex-auth/test.js +9 -0
  107. package/dist/runtime/convex-auth/types.d.ts +18 -0
  108. package/dist/runtime/convex-auth/types.js +0 -0
  109. package/dist/runtime/convex-auth/user-projection.d.ts +106 -0
  110. package/dist/runtime/convex-auth/user-projection.js +128 -0
  111. package/dist/runtime/devtools/auth-proxy-registry.d.ts +4 -0
  112. package/dist/runtime/devtools/auth-proxy-registry.js +47 -0
  113. package/dist/runtime/devtools/bridge-setup.d.ts +21 -0
  114. package/dist/runtime/devtools/bridge-setup.js +165 -0
  115. package/dist/runtime/devtools/path-utils.d.ts +2 -0
  116. package/dist/runtime/devtools/path-utils.js +12 -0
  117. package/dist/runtime/devtools/server.d.ts +2 -0
  118. package/dist/runtime/devtools/server.js +108 -0
  119. package/dist/runtime/devtools/setup-client.d.ts +16 -0
  120. package/dist/runtime/devtools/setup-client.js +43 -0
  121. package/dist/runtime/devtools/sink.d.ts +18 -0
  122. package/dist/runtime/devtools/sink.js +110 -0
  123. package/dist/runtime/devtools/transport.d.ts +16 -0
  124. package/dist/runtime/devtools/transport.js +149 -0
  125. package/dist/runtime/devtools/types.d.ts +167 -0
  126. package/dist/runtime/devtools/types.js +1 -0
  127. package/dist/runtime/devtools/ui/dist/200.html +1 -0
  128. package/dist/runtime/devtools/ui/dist/404.html +1 -0
  129. package/dist/runtime/devtools/ui/dist/_nuxt/C4MASuCh.js +1 -0
  130. package/dist/runtime/devtools/ui/dist/_nuxt/DJHkqtZq.js +3 -0
  131. package/dist/runtime/devtools/ui/dist/_nuxt/DWwrCltl.js +2 -0
  132. package/dist/runtime/devtools/ui/dist/_nuxt/DZ_UiYoH.js +1 -0
  133. package/dist/runtime/devtools/ui/dist/_nuxt/_m9ujb-i.js +1 -0
  134. package/dist/runtime/devtools/ui/dist/_nuxt/builds/latest.json +1 -0
  135. package/dist/runtime/devtools/ui/dist/_nuxt/builds/meta/better-convex-nuxt-devtools.json +1 -0
  136. package/dist/runtime/devtools/ui/dist/_nuxt/entry.DpFDKYZ5.css +1 -0
  137. package/dist/runtime/devtools/ui/dist/_nuxt/error-404.DYUJo5xt.css +1 -0
  138. package/dist/runtime/devtools/ui/dist/_nuxt/error-500.DQdoJJXg.css +1 -0
  139. package/dist/runtime/devtools/ui/dist/convex-logo.svg +5 -0
  140. package/dist/runtime/devtools/ui/dist/index.html +1 -0
  141. package/dist/runtime/errors/index.d.ts +7 -0
  142. package/dist/runtime/errors/index.js +5 -0
  143. package/dist/runtime/middleware/convex-auth.global.d.ts +2 -0
  144. package/dist/runtime/middleware/convex-auth.global.js +31 -0
  145. package/dist/runtime/plugin.auth.client.d.ts +3 -0
  146. package/dist/runtime/plugin.auth.client.js +217 -0
  147. package/dist/runtime/plugin.client.d.ts +3 -0
  148. package/dist/runtime/plugin.client.js +43 -0
  149. package/dist/runtime/plugin.server.d.ts +12 -0
  150. package/dist/runtime/plugin.server.js +98 -0
  151. package/dist/runtime/plugins/convex-call-error-payload.d.ts +14 -0
  152. package/dist/runtime/plugins/convex-call-error-payload.js +12 -0
  153. package/dist/runtime/runtime-context.d.ts +26 -0
  154. package/dist/runtime/runtime-context.js +48 -0
  155. package/dist/runtime/server/api/auth/[...].d.ts +10 -0
  156. package/dist/runtime/server/api/auth/[...].js +509 -0
  157. package/dist/runtime/server/api/auth/authorization-server-metadata.d.ts +2 -0
  158. package/dist/runtime/server/api/auth/authorization-server-metadata.js +6 -0
  159. package/dist/runtime/server/api/auth/body-size.d.ts +15 -0
  160. package/dist/runtime/server/api/auth/body-size.js +179 -0
  161. package/dist/runtime/server/api/auth/headers.d.ts +6 -0
  162. package/dist/runtime/server/api/auth/headers.js +98 -0
  163. package/dist/runtime/server/api/auth/jwks.d.ts +2 -0
  164. package/dist/runtime/server/api/auth/jwks.js +6 -0
  165. package/dist/runtime/server/api/auth/security.d.ts +14 -0
  166. package/dist/runtime/server/api/auth/security.js +85 -0
  167. package/dist/runtime/server/api/release-fingerprint.d.ts +2 -0
  168. package/dist/runtime/server/api/release-fingerprint.js +36 -0
  169. package/dist/runtime/server/index.d.ts +1 -0
  170. package/dist/runtime/server/index.js +3 -0
  171. package/dist/runtime/server/plugins/runtime-config.d.ts +7 -0
  172. package/dist/runtime/server/plugins/runtime-config.js +7 -0
  173. package/dist/runtime/server/utils/auth-snapshot.d.ts +26 -0
  174. package/dist/runtime/server/utils/auth-snapshot.js +165 -0
  175. package/dist/runtime/server/utils/http.d.ts +16 -0
  176. package/dist/runtime/server/utils/http.js +133 -0
  177. package/dist/runtime/server/utils/server-convex-caller.d.ts +31 -0
  178. package/dist/runtime/server/utils/server-convex-caller.js +151 -0
  179. package/dist/runtime/server/utils/server-convex-options.d.ts +83 -0
  180. package/dist/runtime/server/utils/server-convex-options.js +69 -0
  181. package/dist/runtime/server/utils/signed-client-ip.d.ts +6 -0
  182. package/dist/runtime/server/utils/signed-client-ip.js +29 -0
  183. package/dist/runtime/server/utils/ssr-auth-headers.d.ts +26 -0
  184. package/dist/runtime/server/utils/ssr-auth-headers.js +49 -0
  185. package/dist/runtime/server/utils/token-exchange.d.ts +27 -0
  186. package/dist/runtime/server/utils/token-exchange.js +96 -0
  187. package/dist/runtime/shared/auth-cookie.d.ts +4 -0
  188. package/dist/runtime/shared/auth-cookie.js +25 -0
  189. package/dist/runtime/shared/auth-origin.d.ts +15 -0
  190. package/dist/runtime/shared/auth-origin.js +42 -0
  191. package/dist/runtime/shared/bounded-stream.d.ts +2 -0
  192. package/dist/runtime/shared/bounded-stream.js +50 -0
  193. package/dist/runtime/shared/client-ip.d.ts +21 -0
  194. package/dist/runtime/shared/client-ip.js +123 -0
  195. package/dist/runtime/shared/release-fingerprint.d.ts +5 -0
  196. package/dist/runtime/shared/release-fingerprint.js +17 -0
  197. package/dist/runtime/types.d.ts +19 -0
  198. package/dist/runtime/utils/auth-config.d.ts +24 -0
  199. package/dist/runtime/utils/auth-config.js +45 -0
  200. package/dist/runtime/utils/auth-contract.d.ts +22 -0
  201. package/dist/runtime/utils/auth-contract.js +0 -0
  202. package/dist/runtime/utils/auth-errors.d.ts +5 -0
  203. package/dist/runtime/utils/auth-errors.js +22 -0
  204. package/dist/runtime/utils/auth-identity-state.d.ts +4 -0
  205. package/dist/runtime/utils/auth-identity-state.js +8 -0
  206. package/dist/runtime/utils/auth-pending-state.d.ts +13 -0
  207. package/dist/runtime/utils/auth-pending-state.js +4 -0
  208. package/dist/runtime/utils/auth-pending.d.ts +10 -0
  209. package/dist/runtime/utils/auth-pending.js +30 -0
  210. package/dist/runtime/utils/auth-route-protection.d.ts +16 -0
  211. package/dist/runtime/utils/auth-route-protection.js +58 -0
  212. package/dist/runtime/utils/auth-status.d.ts +47 -0
  213. package/dist/runtime/utils/auth-status.js +8 -0
  214. package/dist/runtime/utils/bounded-convex-fetch.d.ts +18 -0
  215. package/dist/runtime/utils/bounded-convex-fetch.js +152 -0
  216. package/dist/runtime/utils/callable-devtools.d.ts +17 -0
  217. package/dist/runtime/utils/callable-devtools.js +49 -0
  218. package/dist/runtime/utils/callable-observer.d.ts +2 -0
  219. package/dist/runtime/utils/callable-observer.js +1 -0
  220. package/dist/runtime/utils/composable-scope.d.ts +2 -0
  221. package/dist/runtime/utils/composable-scope.js +8 -0
  222. package/dist/runtime/utils/config-defaults.d.ts +8 -0
  223. package/dist/runtime/utils/config-defaults.js +8 -0
  224. package/dist/runtime/utils/convex-cache.d.ts +68 -0
  225. package/dist/runtime/utils/convex-cache.js +47 -0
  226. package/dist/runtime/utils/convex-config.d.ts +16 -0
  227. package/dist/runtime/utils/convex-config.js +50 -0
  228. package/dist/runtime/utils/convex-shared.d.ts +67 -0
  229. package/dist/runtime/utils/convex-shared.js +134 -0
  230. package/dist/runtime/utils/identity-changed-error.d.ts +4 -0
  231. package/dist/runtime/utils/identity-changed-error.js +12 -0
  232. package/dist/runtime/utils/identity-key.d.ts +19 -0
  233. package/dist/runtime/utils/identity-key.js +10 -0
  234. package/dist/runtime/utils/integrated-auth-client.d.ts +5 -0
  235. package/dist/runtime/utils/integrated-auth-client.js +0 -0
  236. package/dist/runtime/utils/logger.d.ts +42 -0
  237. package/dist/runtime/utils/logger.js +190 -0
  238. package/dist/runtime/utils/mime-type.d.ts +33 -0
  239. package/dist/runtime/utils/mime-type.js +10 -0
  240. package/dist/runtime/utils/query-execution-gate.d.ts +47 -0
  241. package/dist/runtime/utils/query-execution-gate.js +63 -0
  242. package/dist/runtime/utils/query-execution.d.ts +8 -0
  243. package/dist/runtime/utils/query-execution.js +19 -0
  244. package/dist/runtime/utils/query-foundation.d.ts +20 -0
  245. package/dist/runtime/utils/query-foundation.js +39 -0
  246. package/dist/runtime/utils/query-state.d.ts +12 -0
  247. package/dist/runtime/utils/query-state.js +12 -0
  248. package/dist/runtime/utils/reactive-args.d.ts +5 -0
  249. package/dist/runtime/utils/reactive-args.js +42 -0
  250. package/dist/runtime/utils/runtime-config-normalize.d.ts +22 -0
  251. package/dist/runtime/utils/runtime-config-normalize.js +28 -0
  252. package/dist/runtime/utils/runtime-config.d.ts +4 -0
  253. package/dist/runtime/utils/runtime-config.js +8 -0
  254. package/dist/runtime/utils/sanitize-diagnostic.d.ts +6 -0
  255. package/dist/runtime/utils/sanitize-diagnostic.js +81 -0
  256. package/dist/runtime/utils/shared-helpers.d.ts +46 -0
  257. package/dist/runtime/utils/shared-helpers.js +139 -0
  258. package/dist/runtime/utils/site-url.d.ts +4 -0
  259. package/dist/runtime/utils/site-url.js +15 -0
  260. package/dist/runtime/utils/ssr-pagination-state.d.ts +3 -0
  261. package/dist/runtime/utils/ssr-pagination-state.js +3 -0
  262. package/dist/runtime/utils/types.d.ts +29 -0
  263. package/dist/runtime/utils/types.js +0 -0
  264. package/dist/runtime/utils/upload-core.d.ts +16 -0
  265. package/dist/runtime/utils/upload-core.js +116 -0
  266. package/dist/types.d.mts +21 -0
  267. package/package.json +253 -0
  268. package/security/upstream-convex-better-auth.json +477 -0
@@ -0,0 +1,128 @@
1
+ function resolveAuthIdField(field) {
2
+ const resolved = field ?? "authId";
3
+ const startsWithReservedPrefix = resolved.startsWith("$") || resolved.startsWith("_");
4
+ let containsInvalidCharacter = false;
5
+ for (let index = 0; index < resolved.length && index <= 1024; index += 1) {
6
+ const code = resolved.charCodeAt(index);
7
+ if (code < 32 || code >= 127) {
8
+ containsInvalidCharacter = true;
9
+ break;
10
+ }
11
+ }
12
+ if (resolved.length === 0 || resolved.length > 1024 || startsWithReservedPrefix || containsInvalidCharacter || resolved.includes(".") || resolved === "prototype" || resolved === "constructor") {
13
+ throw new TypeError(
14
+ "[better-convex-nuxt] authIdField must be a valid top-level Convex application field"
15
+ );
16
+ }
17
+ return resolved;
18
+ }
19
+ function withCanonicalAuthId(value, authIdField, authUserId) {
20
+ return { ...value, [authIdField]: authUserId };
21
+ }
22
+ async function findExistingByAuthId(ctx, lookup, authUserId) {
23
+ const query = ctx.db.query(lookup.table);
24
+ return await query.withIndex(
25
+ lookup.index,
26
+ (q) => q.eq(lookup.authIdField, authUserId)
27
+ ).collect();
28
+ }
29
+ async function deleteDuplicateRows(ctx, existing) {
30
+ for (const duplicate of existing.slice(1)) {
31
+ await ctx.db.delete(duplicate._id);
32
+ }
33
+ }
34
+ export function createUserProjectionTriggers(options) {
35
+ const lookup = Object.freeze({
36
+ table: options.table,
37
+ index: options.index,
38
+ authIdField: resolveAuthIdField(options.authIdField)
39
+ });
40
+ return {
41
+ user: {
42
+ onCreate: async (ctx, user) => {
43
+ const now = Date.now();
44
+ const existing = await findExistingByAuthId(ctx, lookup, user.id);
45
+ if (existing.length > 0) {
46
+ await deleteDuplicateRows(ctx, existing);
47
+ return;
48
+ }
49
+ const doc = await options.createDoc({ ctx, user, now });
50
+ await ctx.db.insert(options.table, withCanonicalAuthId(doc, lookup.authIdField, user.id));
51
+ },
52
+ /**
53
+ * Patches the projected row for a Better Auth user update.
54
+ *
55
+ * If `onUpdate` fires before the corresponding `onCreate` has run
56
+ * for this user (e.g. out-of-order trigger delivery, or a webhook race),
57
+ * `findExistingByAuthId` finds nothing and this silently no-ops — the
58
+ * update is dropped rather than queued or retried. The row is created
59
+ * later by `onCreate`, but using `createDoc`'s snapshot at that later
60
+ * time, not the fields this update carried. If your projection needs
61
+ * updates to survive arriving before creation, either make `createDoc`
62
+ * derive from the latest known user state (not just the `onCreate`
63
+ * event's payload) or run `rebuild()` periodically to reconcile drift.
64
+ */
65
+ onUpdate: async (ctx, user, previousUser) => {
66
+ const existing = await findExistingByAuthId(ctx, lookup, user.id);
67
+ const [retained] = existing;
68
+ if (!retained) return;
69
+ await deleteDuplicateRows(ctx, existing);
70
+ if (!options.patchDoc) return;
71
+ const patch = await options.patchDoc({
72
+ ctx,
73
+ user,
74
+ previousUser,
75
+ existing: retained,
76
+ now: Date.now()
77
+ });
78
+ if (!patch || Object.keys(patch).length === 0) return;
79
+ await ctx.db.patch(retained._id, withCanonicalAuthId(patch, lookup.authIdField, user.id));
80
+ },
81
+ onDelete: async (ctx, user) => {
82
+ const existing = await findExistingByAuthId(ctx, lookup, user.id);
83
+ for (const row of existing) {
84
+ await ctx.db.delete(row._id);
85
+ }
86
+ },
87
+ rebuild: async (ctx, users) => {
88
+ const result = {
89
+ inserted: 0,
90
+ patched: 0,
91
+ skipped: 0
92
+ };
93
+ for (const user of users) {
94
+ const now = Date.now();
95
+ const existing = await findExistingByAuthId(ctx, lookup, user.id);
96
+ const [retained] = existing;
97
+ if (!retained) {
98
+ const doc = await options.createDoc({ ctx, user, now });
99
+ await ctx.db.insert(
100
+ options.table,
101
+ withCanonicalAuthId(doc, lookup.authIdField, user.id)
102
+ );
103
+ result.inserted += 1;
104
+ continue;
105
+ }
106
+ await deleteDuplicateRows(ctx, existing);
107
+ if (!options.rebuildDoc) {
108
+ result.skipped += 1;
109
+ continue;
110
+ }
111
+ const patch = await options.rebuildDoc({
112
+ ctx,
113
+ user,
114
+ existing: retained,
115
+ now
116
+ });
117
+ if (!patch || Object.keys(patch).length === 0) {
118
+ result.skipped += 1;
119
+ continue;
120
+ }
121
+ await ctx.db.patch(retained._id, withCanonicalAuthId(patch, lookup.authIdField, user.id));
122
+ result.patched += 1;
123
+ }
124
+ return result;
125
+ }
126
+ }
127
+ };
128
+ }
@@ -0,0 +1,4 @@
1
+ import type { AuthProxyRequest, AuthProxyStats } from './types.js';
2
+ export declare function recordAuthProxyRequest(request: AuthProxyRequest): Promise<void>;
3
+ export declare function getAuthProxyStats(): Promise<AuthProxyStats>;
4
+ export declare function clearAuthProxyStats(): Promise<void>;
@@ -0,0 +1,47 @@
1
+ const MAX_REQUESTS = 20;
2
+ const STORAGE_NAMESPACE = "devtools:convex:auth-proxy";
3
+ const STORAGE_KEY = "requests";
4
+ async function getStorage() {
5
+ const { useStorage } = await import("nitropack/runtime");
6
+ return useStorage(STORAGE_NAMESPACE);
7
+ }
8
+ async function getRequests() {
9
+ const storage = await getStorage();
10
+ const requests = await storage.getItem(STORAGE_KEY);
11
+ return Array.isArray(requests) ? requests : [];
12
+ }
13
+ async function setRequests(requests) {
14
+ const storage = await getStorage();
15
+ await storage.setItem(STORAGE_KEY, requests);
16
+ }
17
+ export async function recordAuthProxyRequest(request) {
18
+ const requests = await getRequests();
19
+ requests.unshift({
20
+ id: request.id,
21
+ path: request.path,
22
+ method: request.method,
23
+ timestamp: request.timestamp,
24
+ ...request.status === void 0 ? {} : { status: request.status },
25
+ ...request.duration === void 0 ? {} : { duration: request.duration },
26
+ ...request.success === void 0 ? {} : { success: request.success }
27
+ });
28
+ if (requests.length > MAX_REQUESTS) {
29
+ requests.length = MAX_REQUESTS;
30
+ }
31
+ await setRequests(requests);
32
+ }
33
+ export async function getAuthProxyStats() {
34
+ const requests = await getRequests();
35
+ const successful = requests.filter((r) => r.success);
36
+ const durations = successful.filter((r) => r.duration !== void 0).map((r) => r.duration);
37
+ return {
38
+ totalRequests: requests.length,
39
+ successCount: successful.length,
40
+ errorCount: requests.filter((r) => !r.success).length,
41
+ avgDuration: durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0,
42
+ recentRequests: [...requests]
43
+ };
44
+ }
45
+ export async function clearAuthProxyStats() {
46
+ await setRequests([]);
47
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * DevTools bridge setup for the Nuxt DevTools integration.
3
+ *
4
+ * This module sets up the communication bridge between the main window
5
+ * and the DevTools iframe using BroadcastChannel.
6
+ */
7
+ import type { Ref } from 'vue';
8
+ import type { DevtoolsSink } from './sink.js';
9
+ import type { AuthWaterfall, ConnectionState } from './types.js';
10
+ /**
11
+ * Setup the DevTools bridge on the window object.
12
+ * Only called in dev mode from plugin.client.ts.
13
+ *
14
+ * @param sink - Per-app bounded diagnostics store
15
+ * @param convexToken - Ref to the current auth token
16
+ * @param convexUser - Ref to the current user data
17
+ * @param convexAuthWaterfall - Ref to the SSR auth waterfall timing data
18
+ * @param readConnectionState - Safe projection of the active Vue runtime connection
19
+ * @param providedInstanceId - Stable application identifier used for explicit UI selection
20
+ */
21
+ export declare function setupDevToolsBridge(sink: DevtoolsSink, convexToken: Ref<string | null>, convexUser: Ref<unknown>, convexAuthWaterfall: Ref<AuthWaterfall | null>, readConnectionState: () => ConnectionState, providedInstanceId?: string): Promise<() => void>;
@@ -0,0 +1,165 @@
1
+ import { toRaw } from "vue";
2
+ import { decodeJwtPayload } from "../utils/convex-shared.js";
3
+ import { createAppDevtoolsTransport, cloneDevtoolsPayload } from "./transport.js";
4
+ export async function setupDevToolsBridge(sink, convexToken, convexUser, convexAuthWaterfall, readConnectionState, providedInstanceId) {
5
+ const decodeJWT = (token) => {
6
+ return decodeJwtPayload(token);
7
+ };
8
+ const bridge = {
9
+ version: "1.1.0",
10
+ getQueries: () => sink.getQueries(),
11
+ getQueryDetail: (id) => sink.getQuery(id),
12
+ subscribeToQueries: (callback) => sink.subscribeToQueries(callback),
13
+ getMutations: () => sink.getMutations(),
14
+ subscribeToMutations: (callback) => sink.subscribeToMutations(callback),
15
+ getAuthState: () => {
16
+ const rawUser = toRaw(convexUser.value);
17
+ const hasToken = !!convexToken.value;
18
+ const hasUser = !!(rawUser && typeof rawUser === "object" && (rawUser.id || rawUser.email));
19
+ const plainUser = hasUser ? cloneDevtoolsPayload(rawUser) : null;
20
+ return {
21
+ isAuthenticated: !!(hasToken && hasUser),
22
+ isPending: false,
23
+ // Could be enhanced to track pending state
24
+ user: plainUser,
25
+ tokenStatus: hasToken ? "valid" : "none"
26
+ };
27
+ },
28
+ getEnhancedAuthState: () => {
29
+ const baseState = bridge.getAuthState();
30
+ const token = convexToken.value;
31
+ if (!token) {
32
+ return {
33
+ ...baseState,
34
+ claims: void 0,
35
+ issuedAt: void 0,
36
+ expiresAt: void 0,
37
+ expiresInSeconds: void 0
38
+ };
39
+ }
40
+ const claims = decodeJWT(token);
41
+ const now = Math.floor(Date.now() / 1e3);
42
+ const expiresAt = claims?.exp;
43
+ const expiresInSeconds = expiresAt ? Math.max(0, expiresAt - now) : void 0;
44
+ return {
45
+ ...baseState,
46
+ claims: claims ?? void 0,
47
+ issuedAt: claims?.iat ? claims.iat * 1e3 : void 0,
48
+ expiresAt: expiresAt ? expiresAt * 1e3 : void 0,
49
+ expiresInSeconds
50
+ };
51
+ },
52
+ getConnectionState: readConnectionState,
53
+ getAuthWaterfall: () => {
54
+ const waterfall = convexAuthWaterfall.value;
55
+ if (!waterfall) return null;
56
+ return cloneDevtoolsPayload(toRaw(waterfall));
57
+ },
58
+ getAuthProxyStats: async () => {
59
+ try {
60
+ const response = await fetch("/__convex_devtools__/proxy-stats");
61
+ if (!response.ok) return null;
62
+ return await response.json();
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ };
68
+ const instanceId = providedInstanceId ?? Math.random().toString(36).slice(2, 10);
69
+ const transport = createAppDevtoolsTransport("convex-devtools");
70
+ const onMessage = (event) => {
71
+ const data = event.data;
72
+ if (!data || typeof data !== "object") return;
73
+ const message = data;
74
+ if (message.type === "CONVEX_DEVTOOLS_INIT") {
75
+ transport.postMessage({
76
+ type: "CONVEX_DEVTOOLS_READY",
77
+ instanceId,
78
+ transport: transport.kind
79
+ });
80
+ } else if (message.type === "CONVEX_DEVTOOLS_REQUEST") {
81
+ if (message.instanceId && message.instanceId !== instanceId) {
82
+ return;
83
+ }
84
+ const { id, method, args } = message;
85
+ if (typeof id !== "number" || typeof method !== "string") {
86
+ return;
87
+ }
88
+ try {
89
+ const bridgeMethod = bridge[method];
90
+ if (typeof bridgeMethod === "function") {
91
+ Promise.resolve(bridgeMethod(...args || [])).then((result) => {
92
+ transport.postMessage({
93
+ type: "CONVEX_DEVTOOLS_RESPONSE",
94
+ id,
95
+ result,
96
+ instanceId,
97
+ transport: transport.kind
98
+ });
99
+ }).catch((err) => {
100
+ transport.postMessage({
101
+ type: "CONVEX_DEVTOOLS_RESPONSE",
102
+ id,
103
+ error: err instanceof Error ? err.message : String(err),
104
+ instanceId,
105
+ transport: transport.kind
106
+ });
107
+ });
108
+ } else if (bridgeMethod !== void 0) {
109
+ transport.postMessage({
110
+ type: "CONVEX_DEVTOOLS_RESPONSE",
111
+ id,
112
+ result: bridgeMethod,
113
+ instanceId,
114
+ transport: transport.kind
115
+ });
116
+ } else {
117
+ transport.postMessage({
118
+ type: "CONVEX_DEVTOOLS_RESPONSE",
119
+ id,
120
+ error: `Unknown method: ${method}`,
121
+ instanceId,
122
+ transport: transport.kind
123
+ });
124
+ }
125
+ } catch (err) {
126
+ transport.postMessage({
127
+ type: "CONVEX_DEVTOOLS_RESPONSE",
128
+ id,
129
+ error: err instanceof Error ? err.message : String(err),
130
+ instanceId,
131
+ transport: transport.kind
132
+ });
133
+ }
134
+ }
135
+ };
136
+ transport.addEventListener("message", onMessage);
137
+ const unsubscribeMutations = sink.subscribeToMutations((mutations) => {
138
+ transport.postMessage({
139
+ type: "CONVEX_DEVTOOLS_MUTATIONS",
140
+ mutations,
141
+ instanceId,
142
+ transport: transport.kind
143
+ });
144
+ });
145
+ const unsubscribeQueries = sink.subscribeToQueries((queries) => {
146
+ transport.postMessage({
147
+ type: "CONVEX_DEVTOOLS_QUERIES",
148
+ queries,
149
+ instanceId,
150
+ transport: transport.kind
151
+ });
152
+ });
153
+ let disposed = false;
154
+ const dispose = () => {
155
+ if (disposed) return;
156
+ disposed = true;
157
+ unsubscribeMutations();
158
+ unsubscribeQueries();
159
+ transport.removeEventListener("message", onMessage);
160
+ transport.close();
161
+ };
162
+ const hot = import.meta.hot;
163
+ hot?.dispose(dispose);
164
+ return dispose;
165
+ }
@@ -0,0 +1,2 @@
1
+ export declare function resolveDevtoolsFilePath(outputDir: string, pathname: string): string;
2
+ export declare function isPathInsideDirectory(rootDir: string, filePath: string): boolean;
@@ -0,0 +1,12 @@
1
+ import { join, relative, resolve, sep } from "node:path";
2
+ export function resolveDevtoolsFilePath(outputDir, pathname) {
3
+ const outputDirResolved = resolve(outputDir);
4
+ return resolve(join(outputDirResolved, pathname));
5
+ }
6
+ export function isPathInsideDirectory(rootDir, filePath) {
7
+ const rootResolved = resolve(rootDir);
8
+ const fileResolved = resolve(filePath);
9
+ const rel = relative(rootResolved, fileResolved);
10
+ if (!rel) return false;
11
+ return !rel.startsWith("..") && !rel.includes(`${sep}..`);
12
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | NonSharedBuffer>>;
2
+ export default _default;
@@ -0,0 +1,108 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { defineEventHandler, setHeader, getRequestURL, createError } from "h3";
3
+ import { useRuntimeConfig } from "#imports";
4
+ import { getAuthProxyStats, clearAuthProxyStats } from "./auth-proxy-registry.js";
5
+ import { isPathInsideDirectory, resolveDevtoolsFilePath } from "./path-utils.js";
6
+ const mimeTypes = {
7
+ ".html": "text/html",
8
+ ".js": "application/javascript",
9
+ ".mjs": "application/javascript",
10
+ ".css": "text/css",
11
+ ".json": "application/json",
12
+ ".png": "image/png",
13
+ ".svg": "image/svg+xml",
14
+ ".ico": "image/x-icon",
15
+ ".woff": "font/woff",
16
+ ".woff2": "font/woff2"
17
+ };
18
+ function getMimeType(path) {
19
+ const ext = path.substring(path.lastIndexOf("."));
20
+ return mimeTypes[ext] || "application/octet-stream";
21
+ }
22
+ export default defineEventHandler(async (event) => {
23
+ if (!import.meta.dev) {
24
+ throw createError({ statusCode: 404, statusMessage: "Not Found" });
25
+ }
26
+ const config = useRuntimeConfig();
27
+ const outputDir = typeof config.convexDevtoolsPath === "string" ? config.convexDevtoolsPath : void 0;
28
+ const url = getRequestURL(event);
29
+ let pathname = url.pathname.replace("/__convex_devtools__", "") || "/";
30
+ if (pathname === "/proxy-stats") {
31
+ setHeader(event, "Content-Type", "application/json");
32
+ setHeader(event, "Cache-Control", "no-cache");
33
+ return JSON.stringify(await getAuthProxyStats());
34
+ }
35
+ if (pathname === "/proxy-stats/clear" && event.method === "POST") {
36
+ await clearAuthProxyStats();
37
+ setHeader(event, "Content-Type", "application/json");
38
+ return JSON.stringify({ success: true });
39
+ }
40
+ if (pathname === "/" || pathname === "") {
41
+ pathname = "/index.html";
42
+ }
43
+ if (!outputDir || !existsSync(outputDir)) {
44
+ if (pathname === "/index.html") {
45
+ setHeader(event, "Content-Type", "text/html");
46
+ return getFallbackHtml();
47
+ }
48
+ throw createError({ statusCode: 404, statusMessage: "Not Found" });
49
+ }
50
+ const filePath = resolveDevtoolsFilePath(outputDir, pathname);
51
+ if (!isPathInsideDirectory(outputDir, filePath)) {
52
+ throw createError({ statusCode: 403, statusMessage: "Forbidden" });
53
+ }
54
+ if (!existsSync(filePath)) {
55
+ if (pathname === "/index.html") {
56
+ setHeader(event, "Content-Type", "text/html");
57
+ return getFallbackHtml();
58
+ }
59
+ throw createError({ statusCode: 404, statusMessage: "Not Found" });
60
+ }
61
+ const content = readFileSync(filePath);
62
+ setHeader(event, "Content-Type", getMimeType(pathname));
63
+ setHeader(event, "Cache-Control", "no-cache");
64
+ return content;
65
+ });
66
+ function getFallbackHtml() {
67
+ return `<!DOCTYPE html>
68
+ <html lang="en">
69
+ <head>
70
+ <meta charset="UTF-8">
71
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
72
+ <title>Convex DevTools</title>
73
+ <style>
74
+ body {
75
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
76
+ background: #1e1e1e;
77
+ color: #cccccc;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ height: 100vh;
82
+ margin: 0;
83
+ }
84
+ .message {
85
+ text-align: center;
86
+ padding: 40px;
87
+ }
88
+ .message h1 {
89
+ color: #ee8944;
90
+ margin-bottom: 16px;
91
+ }
92
+ code {
93
+ background: #252526;
94
+ padding: 4px 8px;
95
+ border-radius: 4px;
96
+ font-size: 13px;
97
+ }
98
+ </style>
99
+ </head>
100
+ <body>
101
+ <div class="message">
102
+ <h1>DevTools Not Built</h1>
103
+ <p>The DevTools UI needs to be built first.</p>
104
+ <p style="margin-top: 16px;">Run: <code>pnpm run build:devtools</code></p>
105
+ </div>
106
+ </body>
107
+ </html>`;
108
+ }
@@ -0,0 +1,16 @@
1
+ import type { Ref } from 'vue';
2
+ import type { ConvexRuntimeContext } from '../runtime-context.js';
3
+ import type { Logger } from '../utils/logger.js';
4
+ import type { AuthWaterfall } from './types.js';
5
+ interface NuxtDevtoolsClientInput {
6
+ runtime: ConvexRuntimeContext;
7
+ token: Ref<string | null>;
8
+ user: Ref<unknown>;
9
+ waterfall: Ref<AuthWaterfall | null>;
10
+ instanceId: string;
11
+ logger: Logger;
12
+ onDispose(dispose: () => void): void;
13
+ }
14
+ /** Install Nuxt's diagnostics adapter without exposing the Vue runtime's raw client or credentials. */
15
+ export declare function setupNuxtDevtoolsClient(input: NuxtDevtoolsClientInput): void;
16
+ export {};
@@ -0,0 +1,43 @@
1
+ function readConnection(runtime) {
2
+ const connection = runtime.attachment.connection?.snapshot();
3
+ if (!connection) {
4
+ return {
5
+ isConnected: false,
6
+ hasEverConnected: false,
7
+ connectionRetries: 0,
8
+ inflightRequests: 0
9
+ };
10
+ }
11
+ const explicitInflight = connection.inflightMutations + connection.inflightActions;
12
+ return {
13
+ isConnected: connection.isWebSocketConnected,
14
+ hasEverConnected: connection.hasEverConnected,
15
+ connectionRetries: connection.connectionRetries,
16
+ inflightRequests: explicitInflight > 0 ? explicitInflight : Number(connection.hasInflightRequests)
17
+ };
18
+ }
19
+ export function setupNuxtDevtoolsClient(input) {
20
+ void Promise.all([import("./bridge-setup"), import("./sink")]).then(async ([{ setupDevToolsBridge }, { createDevtoolsSink }]) => {
21
+ const sink = createDevtoolsSink();
22
+ const detachSink = input.runtime.attachDevtoolsSink(sink);
23
+ if (!detachSink) return;
24
+ let disposeBridge;
25
+ try {
26
+ disposeBridge = await setupDevToolsBridge(
27
+ sink,
28
+ input.token,
29
+ input.user,
30
+ input.waterfall,
31
+ () => readConnection(input.runtime),
32
+ input.instanceId
33
+ );
34
+ } catch (error) {
35
+ detachSink();
36
+ throw error;
37
+ }
38
+ input.onDispose(() => {
39
+ disposeBridge();
40
+ detachSink();
41
+ });
42
+ }).catch((error) => input.logger.debug("DevTools bridge setup failed", error));
43
+ }
@@ -0,0 +1,18 @@
1
+ import type { MutationEntry, QueryRegistryEntry } from './types.js';
2
+ type QuerySubscriber = (entries: QueryRegistryEntry[]) => void;
3
+ type MutationSubscriber = (entries: MutationEntry[]) => void;
4
+ export interface DevtoolsSink {
5
+ getQueries(): QueryRegistryEntry[];
6
+ getQuery(id: string): QueryRegistryEntry | undefined;
7
+ upsertQuery(entry: Omit<QueryRegistryEntry, 'lastUpdated'>): void;
8
+ removeQuery(id: string): void;
9
+ subscribeToQueries(callback: QuerySubscriber): () => void;
10
+ registerMutation(entry: Omit<MutationEntry, 'id'>): string;
11
+ updateMutation(id: string, update: Partial<Pick<MutationEntry, 'state' | 'result' | 'error' | 'settledAt' | 'duration'>>): void;
12
+ getMutations(): MutationEntry[];
13
+ subscribeToMutations(callback: MutationSubscriber): () => void;
14
+ clearIdentityOwned(): void;
15
+ dispose(): void;
16
+ }
17
+ export declare function createDevtoolsSink(): DevtoolsSink;
18
+ export {};
@@ -0,0 +1,110 @@
1
+ import { sanitizeDiagnosticValue } from "../utils/sanitize-diagnostic.js";
2
+ const MAX_MUTATIONS = 50;
3
+ const MAX_QUERIES = 200;
4
+ function createId() {
5
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
6
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
7
+ }
8
+ export function createDevtoolsSink() {
9
+ const queries = /* @__PURE__ */ new Map();
10
+ const mutations = /* @__PURE__ */ new Map();
11
+ const querySubscribers = /* @__PURE__ */ new Set();
12
+ const mutationSubscribers = /* @__PURE__ */ new Set();
13
+ let disposed = false;
14
+ const cloneEntry = (entry) => sanitizeDiagnosticValue(entry);
15
+ const getQueries = () => [...queries.values()].map(cloneEntry);
16
+ const getMutations = () => [...mutations.values()].sort((left, right) => right.startedAt - left.startedAt).map(cloneEntry);
17
+ const notifyQueries = () => {
18
+ for (const subscriber of querySubscribers) {
19
+ try {
20
+ subscriber(getQueries());
21
+ } catch {
22
+ }
23
+ }
24
+ };
25
+ const notifyMutations = () => {
26
+ for (const subscriber of mutationSubscribers) {
27
+ try {
28
+ subscriber(getMutations());
29
+ } catch {
30
+ }
31
+ }
32
+ };
33
+ return {
34
+ getQueries,
35
+ getQuery: (id) => {
36
+ const entry = queries.get(id);
37
+ return entry ? cloneEntry(entry) : void 0;
38
+ },
39
+ upsertQuery(entry) {
40
+ if (disposed) return;
41
+ queries.delete(entry.id);
42
+ queries.set(entry.id, {
43
+ ...entry,
44
+ args: sanitizeDiagnosticValue(entry.args),
45
+ data: sanitizeDiagnosticValue(entry.data),
46
+ error: entry.error === void 0 ? void 0 : String(sanitizeDiagnosticValue(entry.error)),
47
+ lastUpdated: Date.now()
48
+ });
49
+ while (queries.size > MAX_QUERIES) queries.delete(queries.keys().next().value);
50
+ notifyQueries();
51
+ },
52
+ removeQuery(id) {
53
+ if (queries.delete(id)) notifyQueries();
54
+ },
55
+ subscribeToQueries(callback) {
56
+ if (disposed) return () => {
57
+ };
58
+ querySubscribers.add(callback);
59
+ callback(getQueries());
60
+ return () => querySubscribers.delete(callback);
61
+ },
62
+ registerMutation(entry) {
63
+ if (disposed) return "";
64
+ const id = createId();
65
+ mutations.set(id, {
66
+ ...entry,
67
+ id,
68
+ args: sanitizeDiagnosticValue(entry.args)
69
+ });
70
+ while (mutations.size > MAX_MUTATIONS) mutations.delete(mutations.keys().next().value);
71
+ notifyMutations();
72
+ return id;
73
+ },
74
+ updateMutation(id, update) {
75
+ if (disposed) return;
76
+ const existing = mutations.get(id);
77
+ if (!existing) return;
78
+ mutations.set(id, {
79
+ ...existing,
80
+ ...update,
81
+ ...update.result === void 0 ? {} : { result: sanitizeDiagnosticValue(update.result) },
82
+ ...update.error === void 0 ? {} : { error: String(sanitizeDiagnosticValue(update.error)) }
83
+ });
84
+ notifyMutations();
85
+ },
86
+ getMutations,
87
+ subscribeToMutations(callback) {
88
+ if (disposed) return () => {
89
+ };
90
+ mutationSubscribers.add(callback);
91
+ callback(getMutations());
92
+ return () => mutationSubscribers.delete(callback);
93
+ },
94
+ clearIdentityOwned() {
95
+ if (disposed) return;
96
+ queries.clear();
97
+ mutations.clear();
98
+ notifyQueries();
99
+ notifyMutations();
100
+ },
101
+ dispose() {
102
+ if (disposed) return;
103
+ disposed = true;
104
+ queries.clear();
105
+ mutations.clear();
106
+ querySubscribers.clear();
107
+ mutationSubscribers.clear();
108
+ }
109
+ };
110
+ }