@bandeira-tech/b3nd-save 0.8.1 → 0.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 (161) hide show
  1. package/esm/clients/save-client.d.ts +63 -29
  2. package/esm/clients/save-client.d.ts.map +1 -1
  3. package/esm/clients/save-client.js +148 -68
  4. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/mod.d.ts +1 -1
  5. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/mod.d.ts.map +1 -0
  6. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.d.ts.map +1 -1
  7. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.d.ts.map +1 -1
  8. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encrypt/mod.d.ts +1 -0
  9. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/mod.d.ts.map +1 -0
  10. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encrypt/mod.js +1 -0
  11. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.d.ts +6 -0
  12. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.d.ts.map +1 -0
  13. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.js +12 -0
  14. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.d.ts.map +1 -1
  15. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.d.ts.map +1 -1
  16. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.d.ts.map +1 -1
  17. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.d.ts.map +1 -1
  18. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.d.ts.map +1 -1
  19. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.d.ts.map +1 -1
  20. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/path-vector.d.ts +3 -1
  21. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/network/policies/path-vector.d.ts.map +1 -0
  22. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/path-vector.js +3 -1
  23. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.d.ts.map +1 -1
  24. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.d.ts.map +1 -1
  25. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.d.ts.map +1 -1
  26. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/connection.d.ts +13 -5
  27. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/connection.d.ts.map +1 -0
  28. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/connection.js +1 -1
  29. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.d.ts.map +1 -1
  30. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.d.ts.map +1 -1
  31. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.d.ts.map +1 -1
  32. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.d.ts.map +1 -1
  33. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.d.ts.map +1 -1
  34. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/rig.d.ts +3 -0
  35. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/rig.d.ts.map +1 -0
  36. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/rig.js +46 -4
  37. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/types.d.ts +4 -4
  38. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/types.d.ts.map +1 -0
  39. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/types/types.d.ts +119 -80
  40. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.d.ts.map +1 -0
  41. package/esm/dispatch.d.ts +116 -3
  42. package/esm/dispatch.d.ts.map +1 -1
  43. package/esm/dispatch.js +381 -5
  44. package/esm/elasticsearch/store.d.ts +124 -25
  45. package/esm/elasticsearch/store.d.ts.map +1 -1
  46. package/esm/elasticsearch/store.js +451 -92
  47. package/esm/entity-store.d.ts +1 -1
  48. package/esm/errors.d.ts +1 -1
  49. package/esm/errors.js +1 -1
  50. package/esm/fs/mod.d.ts +26 -0
  51. package/esm/fs/mod.d.ts.map +1 -1
  52. package/esm/fs/store.d.ts +100 -23
  53. package/esm/fs/store.d.ts.map +1 -1
  54. package/esm/fs/store.js +250 -66
  55. package/esm/glob.d.ts +114 -0
  56. package/esm/glob.d.ts.map +1 -0
  57. package/esm/glob.js +352 -0
  58. package/esm/identifiers.d.ts +18 -0
  59. package/esm/identifiers.d.ts.map +1 -0
  60. package/esm/identifiers.js +19 -0
  61. package/esm/indexeddb/fields.d.ts +32 -0
  62. package/esm/indexeddb/fields.d.ts.map +1 -0
  63. package/esm/indexeddb/fields.js +51 -0
  64. package/esm/indexeddb/store.d.ts +72 -17
  65. package/esm/indexeddb/store.d.ts.map +1 -1
  66. package/esm/indexeddb/store.js +362 -106
  67. package/esm/ipfs/mod.d.ts +19 -0
  68. package/esm/ipfs/mod.d.ts.map +1 -1
  69. package/esm/ipfs/mod.js +19 -0
  70. package/esm/ipfs/store.d.ts +82 -21
  71. package/esm/ipfs/store.d.ts.map +1 -1
  72. package/esm/ipfs/store.js +269 -68
  73. package/esm/json-fields.d.ts +61 -0
  74. package/esm/json-fields.d.ts.map +1 -0
  75. package/esm/json-fields.js +121 -0
  76. package/esm/localstorage/store.d.ts +89 -21
  77. package/esm/localstorage/store.d.ts.map +1 -1
  78. package/esm/localstorage/store.js +247 -54
  79. package/esm/memory/store.d.ts +35 -4
  80. package/esm/memory/store.d.ts.map +1 -1
  81. package/esm/memory/store.js +69 -47
  82. package/esm/mod.d.ts +2 -0
  83. package/esm/mod.d.ts.map +1 -1
  84. package/esm/mod.js +1 -0
  85. package/esm/mongo/fields.d.ts +53 -0
  86. package/esm/mongo/fields.d.ts.map +1 -0
  87. package/esm/mongo/fields.js +71 -0
  88. package/esm/mongo/mod.d.ts +19 -4
  89. package/esm/mongo/mod.d.ts.map +1 -1
  90. package/esm/mongo/mod.js +4 -2
  91. package/esm/mongo/store.d.ts +114 -25
  92. package/esm/mongo/store.d.ts.map +1 -1
  93. package/esm/mongo/store.js +355 -102
  94. package/esm/postgres/store.d.ts +50 -1
  95. package/esm/postgres/store.d.ts.map +1 -1
  96. package/esm/postgres/store.js +133 -13
  97. package/esm/read.d.ts +54 -7
  98. package/esm/read.d.ts.map +1 -1
  99. package/esm/read.js +141 -16
  100. package/esm/s3/store.d.ts +125 -23
  101. package/esm/s3/store.d.ts.map +1 -1
  102. package/esm/s3/store.js +313 -57
  103. package/esm/sqlite/columns.d.ts +33 -0
  104. package/esm/sqlite/columns.d.ts.map +1 -0
  105. package/esm/sqlite/columns.js +51 -0
  106. package/esm/sqlite/store.d.ts +73 -25
  107. package/esm/sqlite/store.d.ts.map +1 -1
  108. package/esm/sqlite/store.js +411 -107
  109. package/esm/types.d.ts +1 -1
  110. package/esm/url.d.ts +122 -19
  111. package/esm/url.d.ts.map +1 -1
  112. package/esm/url.js +218 -24
  113. package/esm/walk-via-ls.d.ts +122 -0
  114. package/esm/walk-via-ls.d.ts.map +1 -0
  115. package/esm/walk-via-ls.js +178 -0
  116. package/package.json +1 -1
  117. package/esm/byte-entity-shim.d.ts +0 -47
  118. package/esm/byte-entity-shim.d.ts.map +0 -1
  119. package/esm/byte-entity-shim.js +0 -138
  120. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.d.ts.map +0 -1
  121. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encrypt/mod.d.ts.map +0 -1
  122. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/path-vector.d.ts.map +0 -1
  123. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/connection.d.ts.map +0 -1
  124. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/rig.d.ts.map +0 -1
  125. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/types.d.ts.map +0 -1
  126. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.d.ts.map +0 -1
  127. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/mod.js +0 -0
  128. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.d.ts +0 -0
  129. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.js +0 -0
  130. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.d.ts +0 -0
  131. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.js +0 -0
  132. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.d.ts +0 -0
  133. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.js +0 -0
  134. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.d.ts +0 -0
  135. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.js +0 -0
  136. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.d.ts +0 -0
  137. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.js +0 -0
  138. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.d.ts +0 -0
  139. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.js +0 -0
  140. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.d.ts +0 -0
  141. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.js +0 -0
  142. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.d.ts +0 -0
  143. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.js +0 -0
  144. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.d.ts +0 -0
  145. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.js +0 -0
  146. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.d.ts +0 -0
  147. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.js +0 -0
  148. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.d.ts +0 -0
  149. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.js +0 -0
  150. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.d.ts +0 -0
  151. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.js +0 -0
  152. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.d.ts +0 -0
  153. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.js +0 -0
  154. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.d.ts +0 -0
  155. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.js +0 -0
  156. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.d.ts +0 -0
  157. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.js +0 -0
  158. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.d.ts +0 -0
  159. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.js +0 -0
  160. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/types.js +0 -0
  161. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/types/types.js +0 -0
package/esm/url.d.ts CHANGED
@@ -9,24 +9,46 @@
9
9
  * (memory, postgres, mongo, sqlite, fs, ipfs, s3, elasticsearch,
10
10
  * localstorage, indexeddb) honor the grammar defined here.
11
11
  *
12
- * A **url** = a uri (the canonical resource identifier) plus a query
13
- * string of read parameters. The uri is the routing identity used for
14
- * storage keys and observe-match keys. The query string carries
15
- * request-time-only directives that shape the response without
16
- * changing identity.
12
+ * A **url** = a uri (the canonical resource identifier, possibly carrying
13
+ * wildcards in its path) plus a query string of read parameters. The
14
+ * literal portion of the uri (everything before the first wildcard) is
15
+ * the routing identity used for storage keys and observe-match keys.
16
+ * The query string carries request-time-only directives that shape the
17
+ * response without changing identity.
17
18
  *
18
- * Grammar:
19
+ * Grammar (v2 — see `.cc-chat/20260625121936-grammar-shape/output.md`):
19
20
  *
20
- * <uri>[?fn=<fn>][&<param>=<value>...][&x-<ns>.<key>=<value>...]
21
+ * <uri-with-glob>[?fn=<fn>][&<param>=<value>...][&x-<ns>.<key>=<value>...]
22
+ *
23
+ * Glob tokens in `<uri-with-glob>`'s path:
24
+ * - `?` exactly one non-`/` character
25
+ * - `*` zero or more non-`/` characters (one segment)
26
+ * - `**` zero or more segments (any chars including `/`); **find-only**
21
27
  *
22
28
  * Reserved `fn` values:
23
- * - `read` — point read (default for non-trailing-slash uris)
24
- * - `ls` — list under a prefix (default when uri ends with `/`)
25
- * - `count` count of entries under a prefix
29
+ * - `read` — point read (default for bare uris)
30
+ * - `ls` — list direct children (URI has `?`/`*` but no `**`)
31
+ * - `find` recursive walk (URI contains `**`)
32
+ * - `count` — count of entries
26
33
  * - `x-*.*` — provider-defined extension functions
27
34
  *
35
+ * Validation rules (§3.4 — silent-default-trap mitigation):
36
+ * - A URI with wildcards in its path MUST carry an explicit `?fn=`.
37
+ * - `?fn=ls` rejects URIs containing `**`.
38
+ * - `?fn=find` requires `**` (or a legacy `?pattern=` carrying `**`).
39
+ * - `?fn=count` accepts any URI shape.
40
+ * - A bare URI (no wildcards) keeps today's behavior — `?fn=` may be
41
+ * omitted; defaults to `read` (no trailing slash) or `ls` (trailing
42
+ * slash, equivalent to `<uri>*?fn=ls`).
43
+ *
44
+ * Dual-accept transition: `?pattern=<glob>` still parses (logged as
45
+ * deprecated once per process). Internally normalized to the same
46
+ * `{prefix, glob}` shape as the URI-embedded form. Removal scheduled
47
+ * one minor version out.
48
+ *
28
49
  * Standard params (meaningful only for some fns; stores ignore the rest):
29
- * `limit`, `page`, `cursor`, `sortBy`, `sortOrder`, `pattern`, `format`.
50
+ * `limit`, `page`, `cursor`, `sortBy`, `sortOrder`, `pattern`, `format`,
51
+ * `fields`.
30
52
  *
31
53
  * Anything matching `x-<ns>.<key>` lands in the `ext` map and is passed
32
54
  * opaquely to the executing store. Callers inspect ext entries by
@@ -44,12 +66,22 @@ export interface ReadParams {
44
66
  sortOrder?: string;
45
67
  pattern?: string;
46
68
  format?: string;
69
+ /**
70
+ * Comma-separated list of field names to project from each returned
71
+ * record. Applies to `fn=read` and `fn=ls&format=full`. Fields not
72
+ * declared in the entity meta are silently absent from the
73
+ * projection — the store does not error on unknown projection
74
+ * fields, they simply do not appear. `format=uris` ignores `fields`
75
+ * since it returns no record payloads.
76
+ */
77
+ fields?: string[];
47
78
  }
48
79
  /**
49
80
  * A parsed url decomposed into routing identity (`uri`), function
50
81
  * (`fn`), standard read parameters (`params`), and the `x-*` extension
51
82
  * bag (`ext`), plus the structural fields shared with WHATWG URL
52
- * (`protocol`, `hostname`, `path`, `program`).
83
+ * (`protocol`, `hostname`, `path`, `program`), plus the v2 glob split
84
+ * (`glob`).
53
85
  */
54
86
  export interface ParsedUrl {
55
87
  /** Scheme without trailing `://` (e.g. `"mutable"`, `"b3nd"`). */
@@ -60,15 +92,64 @@ export interface ParsedUrl {
60
92
  path: string;
61
93
  /** `protocol://hostname` — the routing root above the path. */
62
94
  program: string;
63
- /** Full routing identity: `program` + `path`. Query is stripped. */
95
+ /**
96
+ * Routing identity. When the source url contains no wildcards in its
97
+ * path, this is the full URI as written. When the source contains
98
+ * wildcards, this is the **literal prefix** — everything before the
99
+ * first wildcard segment, including the trailing `/`. Backends key
100
+ * storage / observe matching off this value.
101
+ */
64
102
  uri: string;
65
- /** Reserved fn (`read`/`ls`/`count`) or `x-<ns>.<name>` extension. */
103
+ /**
104
+ * Glob tail extracted from the source URI's path (v2 §3.3). Empty
105
+ * string when the source carried no wildcards in its path. When the
106
+ * caller used the dual-accept `?pattern=` form, this carries that
107
+ * value verbatim (back-compat normalization).
108
+ */
109
+ glob: string;
110
+ /** Reserved fn (`read`/`ls`/`find`/`count`) or `x-<ns>.<name>` extension. */
66
111
  fn: string;
67
- /** Standard read params (numeric ones coerced). */
112
+ /**
113
+ * Standard read params. `params.pattern` is populated from either
114
+ * the URI-embedded glob (preferred) or the legacy `?pattern=` query
115
+ * param (deprecated) — both end up here so call sites that read
116
+ * `params.pattern` keep working through the transition.
117
+ */
68
118
  params: ReadParams;
69
119
  /** Bag of `x-*` extension query params (every key starts with `x-`). */
70
120
  ext: Record<string, string>;
71
121
  }
122
+ /**
123
+ * Split a uri's path into its literal prefix and its glob tail.
124
+ *
125
+ * Walks the path looking for the first segment that contains a glob
126
+ * metacharacter (`*` or `?`). Returns the literal prefix (everything
127
+ * up to and including the `/` before that segment) and the glob tail
128
+ * (the wildcard segment and everything after it). When no wildcards
129
+ * are present, returns the whole URI as `prefix` and `""` as `glob`.
130
+ *
131
+ * The walk is path-aware so `?` is treated as a wildcard inside path
132
+ * segments only (the function never sees a query string — callers
133
+ * strip `?<query>` first).
134
+ *
135
+ * Examples:
136
+ * "x://a/b/c" → { prefix: "x://a/b/c", glob: "" }
137
+ * "x://a/b/**" → { prefix: "x://a/b/", glob: "**" }
138
+ * "x://a/**\/x.md" → { prefix: "x://a/", glob: "**\/x.md" }
139
+ * "x://a/al*" → { prefix: "x://a/", glob: "al*" }
140
+ * "x://a/al?" → { prefix: "x://a/", glob: "al?" }
141
+ * "**" → { prefix: "", glob: "**" }
142
+ *
143
+ * Note: the input must already have `?<query>` stripped — the function
144
+ * has no way to distinguish a literal `?` in the path from a query
145
+ * separator. `parseUrl` handles the split before calling.
146
+ */
147
+ export declare function splitLocatorGlob(uri: string): {
148
+ prefix: string;
149
+ glob: string;
150
+ };
151
+ /** Test-only hook to reset the deprecation gate between tests. */
152
+ export declare function _resetPatternDeprecationLogForTests(): void;
72
153
  /**
73
154
  * Parse a url into its structural fields, function, params, and extensions.
74
155
  *
@@ -77,11 +158,23 @@ export interface ParsedUrl {
77
158
  * (e.g. `b3nd://count/mutable://users/`) stays inside `path` — no
78
159
  * special-casing.
79
160
  *
80
- * Defaults:
81
- * - `fn=read` for uris without a trailing slash
82
- * - `fn=ls` for uris with a trailing slash
161
+ * Glob extraction: the path is scanned for the first wildcard segment
162
+ * (`splitLocatorGlob`). When wildcards are present, `parsed.uri` is the
163
+ * literal prefix; `parsed.glob` is the wildcard tail; `parsed.params.pattern`
164
+ * is set to the glob so downstream code can read either field. When the
165
+ * caller used the legacy `?pattern=` form instead, the same fields are
166
+ * populated and a deprecation is logged once per process.
83
167
  *
84
- * Numeric params (`limit`, `page`) are coerced; throws on NaN.
168
+ * Verb defaults & validation (v2 §3.4):
169
+ * - bare URI (no wildcards), no `?fn=` → defaults to `read` for non-
170
+ * trailing-slash, `ls` for trailing-slash (legacy convenience)
171
+ * - any URI with wildcards in the path → `?fn=` is REQUIRED
172
+ * - `**` in the URI is valid only under `?fn=find` (or `?fn=count`)
173
+ * - `*`/`?` (no `**`) is valid under `?fn=ls` or `?fn=find` or `?fn=count`
174
+ * - `?fn=count` accepts any URI shape
175
+ *
176
+ * Numeric params (`limit`, `page`) are coerced; throws on NaN. Unknown
177
+ * standard params throw.
85
178
  */
86
179
  export declare function parseUrl(url: string): ParsedUrl;
87
180
  /**
@@ -91,6 +184,12 @@ export declare function parseUrl(url: string): ParsedUrl;
91
184
  *
92
185
  * Omits `fn=` when it matches the trailing-slash default. Throws if any
93
186
  * `ext` key does not start with `x-`.
187
+ *
188
+ * Note: `buildUrl` writes the URI verbatim — it does not splice a glob
189
+ * back into the path. Callers that have a `ParsedUrl` produced by
190
+ * `parseUrl` from a URI-embedded-glob source should reconstruct the
191
+ * full url manually if they need to round-trip the original string; the
192
+ * cheap path is `<originalUrl>` which they already have.
94
193
  */
95
194
  export declare function buildUrl(parsed: {
96
195
  uri: string;
@@ -103,6 +202,10 @@ export declare function buildUrl(parsed: {
103
202
  * request-time-only and never participates in routing or storage
104
203
  * keying. Cheap helper for stores and callers that need the bare uri
105
204
  * without paying for full parse.
205
+ *
206
+ * Note: this returns the URI as written, **including** any embedded
207
+ * glob (`<uri-with-glob>`). For just the literal routing prefix, parse
208
+ * the url and read `parsed.uri`.
106
209
  */
107
210
  export declare function uriOf(url: string): string;
108
211
  //# sourceMappingURL=url.d.ts.map
package/esm/url.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAIH;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,sEAAsE;IACtE,EAAE,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,MAAM,EAAE,UAAU,CAAC;IACnB,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CA8D/C;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,GAAG,MAAM,CAwBT;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGzC"}
1
+ {"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAIH;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,EAAE,EAAE,MAAM,CAAC;IACX;;;;;OAKG;IACH,MAAM,EAAE,UAAU,CAAC;IACnB,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,GACV;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAuClC;AAcD,kEAAkE;AAClE,wBAAgB,mCAAmC,IAAI,IAAI,CAE1D;AAID;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAkJ/C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,GAAG,MAAM,CAwBT;AAED;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGzC"}
package/esm/url.js CHANGED
@@ -9,29 +9,129 @@
9
9
  * (memory, postgres, mongo, sqlite, fs, ipfs, s3, elasticsearch,
10
10
  * localstorage, indexeddb) honor the grammar defined here.
11
11
  *
12
- * A **url** = a uri (the canonical resource identifier) plus a query
13
- * string of read parameters. The uri is the routing identity used for
14
- * storage keys and observe-match keys. The query string carries
15
- * request-time-only directives that shape the response without
16
- * changing identity.
12
+ * A **url** = a uri (the canonical resource identifier, possibly carrying
13
+ * wildcards in its path) plus a query string of read parameters. The
14
+ * literal portion of the uri (everything before the first wildcard) is
15
+ * the routing identity used for storage keys and observe-match keys.
16
+ * The query string carries request-time-only directives that shape the
17
+ * response without changing identity.
17
18
  *
18
- * Grammar:
19
+ * Grammar (v2 — see `.cc-chat/20260625121936-grammar-shape/output.md`):
19
20
  *
20
- * <uri>[?fn=<fn>][&<param>=<value>...][&x-<ns>.<key>=<value>...]
21
+ * <uri-with-glob>[?fn=<fn>][&<param>=<value>...][&x-<ns>.<key>=<value>...]
22
+ *
23
+ * Glob tokens in `<uri-with-glob>`'s path:
24
+ * - `?` exactly one non-`/` character
25
+ * - `*` zero or more non-`/` characters (one segment)
26
+ * - `**` zero or more segments (any chars including `/`); **find-only**
21
27
  *
22
28
  * Reserved `fn` values:
23
- * - `read` — point read (default for non-trailing-slash uris)
24
- * - `ls` — list under a prefix (default when uri ends with `/`)
25
- * - `count` count of entries under a prefix
29
+ * - `read` — point read (default for bare uris)
30
+ * - `ls` — list direct children (URI has `?`/`*` but no `**`)
31
+ * - `find` recursive walk (URI contains `**`)
32
+ * - `count` — count of entries
26
33
  * - `x-*.*` — provider-defined extension functions
27
34
  *
35
+ * Validation rules (§3.4 — silent-default-trap mitigation):
36
+ * - A URI with wildcards in its path MUST carry an explicit `?fn=`.
37
+ * - `?fn=ls` rejects URIs containing `**`.
38
+ * - `?fn=find` requires `**` (or a legacy `?pattern=` carrying `**`).
39
+ * - `?fn=count` accepts any URI shape.
40
+ * - A bare URI (no wildcards) keeps today's behavior — `?fn=` may be
41
+ * omitted; defaults to `read` (no trailing slash) or `ls` (trailing
42
+ * slash, equivalent to `<uri>*?fn=ls`).
43
+ *
44
+ * Dual-accept transition: `?pattern=<glob>` still parses (logged as
45
+ * deprecated once per process). Internally normalized to the same
46
+ * `{prefix, glob}` shape as the URI-embedded form. Removal scheduled
47
+ * one minor version out.
48
+ *
28
49
  * Standard params (meaningful only for some fns; stores ignore the rest):
29
- * `limit`, `page`, `cursor`, `sortBy`, `sortOrder`, `pattern`, `format`.
50
+ * `limit`, `page`, `cursor`, `sortBy`, `sortOrder`, `pattern`, `format`,
51
+ * `fields`.
30
52
  *
31
53
  * Anything matching `x-<ns>.<key>` lands in the `ext` map and is passed
32
54
  * opaquely to the executing store. Callers inspect ext entries by
33
55
  * their flat string key (e.g. `ext["x-feed.cursor"]`).
34
56
  */
57
+ /**
58
+ * Split a uri's path into its literal prefix and its glob tail.
59
+ *
60
+ * Walks the path looking for the first segment that contains a glob
61
+ * metacharacter (`*` or `?`). Returns the literal prefix (everything
62
+ * up to and including the `/` before that segment) and the glob tail
63
+ * (the wildcard segment and everything after it). When no wildcards
64
+ * are present, returns the whole URI as `prefix` and `""` as `glob`.
65
+ *
66
+ * The walk is path-aware so `?` is treated as a wildcard inside path
67
+ * segments only (the function never sees a query string — callers
68
+ * strip `?<query>` first).
69
+ *
70
+ * Examples:
71
+ * "x://a/b/c" → { prefix: "x://a/b/c", glob: "" }
72
+ * "x://a/b/**" → { prefix: "x://a/b/", glob: "**" }
73
+ * "x://a/**\/x.md" → { prefix: "x://a/", glob: "**\/x.md" }
74
+ * "x://a/al*" → { prefix: "x://a/", glob: "al*" }
75
+ * "x://a/al?" → { prefix: "x://a/", glob: "al?" }
76
+ * "**" → { prefix: "", glob: "**" }
77
+ *
78
+ * Note: the input must already have `?<query>` stripped — the function
79
+ * has no way to distinguish a literal `?` in the path from a query
80
+ * separator. `parseUrl` handles the split before calling.
81
+ */
82
+ export function splitLocatorGlob(uri) {
83
+ // Find the scheme boundary; wildcards before the path are nonsense
84
+ // but we don't reject them here — splitLocatorGlob is a pure
85
+ // structural split. The caller (parseUrl) sees the result and the
86
+ // grammar validator decides what's legal.
87
+ const schemeIdx = uri.indexOf("://");
88
+ let pathStart;
89
+ if (schemeIdx < 0) {
90
+ pathStart = 0;
91
+ }
92
+ else {
93
+ const rest = uri.slice(schemeIdx + 3);
94
+ const slash = rest.indexOf("/");
95
+ pathStart = slash < 0 ? uri.length : schemeIdx + 3 + slash;
96
+ }
97
+ // Walk segment by segment from pathStart. A segment containing `*`
98
+ // or `?` is the first wildcard segment; the split point is the `/`
99
+ // that begins that segment.
100
+ let segStart = pathStart;
101
+ const len = uri.length;
102
+ for (let i = pathStart; i <= len; i++) {
103
+ if (i === len || uri.charCodeAt(i) === 47 /* / */) {
104
+ const seg = uri.slice(segStart, i);
105
+ if (seg.includes("*") || seg.includes("?")) {
106
+ // Prefix is everything up to (and including) the `/` before
107
+ // this segment. segStart points at the first char of the
108
+ // segment, so prefix = uri.slice(0, segStart). When the
109
+ // wildcard segment is at the root (no preceding `/`),
110
+ // segStart === pathStart and prefix is just the program.
111
+ return {
112
+ prefix: uri.slice(0, segStart),
113
+ glob: uri.slice(segStart),
114
+ };
115
+ }
116
+ segStart = i + 1;
117
+ }
118
+ }
119
+ return { prefix: uri, glob: "" };
120
+ }
121
+ // ── Deprecation log gate ────────────────────────────────────────────
122
+ let _patternDeprecationLogged = false;
123
+ function logPatternDeprecationOnce() {
124
+ if (_patternDeprecationLogged)
125
+ return;
126
+ _patternDeprecationLogged = true;
127
+ console.warn("b3nd-save: `?pattern=` query param is deprecated; use URI-embedded glob " +
128
+ '(e.g. "<root>/**?fn=find"). Support will be removed one minor version out.');
129
+ }
130
+ /** Test-only hook to reset the deprecation gate between tests. */
131
+ export function _resetPatternDeprecationLogForTests() {
132
+ _patternDeprecationLogged = false;
133
+ }
134
+ // ── Parser ───────────────────────────────────────────────────────────
35
135
  /**
36
136
  * Parse a url into its structural fields, function, params, and extensions.
37
137
  *
@@ -40,23 +140,35 @@
40
140
  * (e.g. `b3nd://count/mutable://users/`) stays inside `path` — no
41
141
  * special-casing.
42
142
  *
43
- * Defaults:
44
- * - `fn=read` for uris without a trailing slash
45
- * - `fn=ls` for uris with a trailing slash
143
+ * Glob extraction: the path is scanned for the first wildcard segment
144
+ * (`splitLocatorGlob`). When wildcards are present, `parsed.uri` is the
145
+ * literal prefix; `parsed.glob` is the wildcard tail; `parsed.params.pattern`
146
+ * is set to the glob so downstream code can read either field. When the
147
+ * caller used the legacy `?pattern=` form instead, the same fields are
148
+ * populated and a deprecation is logged once per process.
46
149
  *
47
- * Numeric params (`limit`, `page`) are coerced; throws on NaN.
150
+ * Verb defaults & validation (v2 §3.4):
151
+ * - bare URI (no wildcards), no `?fn=` → defaults to `read` for non-
152
+ * trailing-slash, `ls` for trailing-slash (legacy convenience)
153
+ * - any URI with wildcards in the path → `?fn=` is REQUIRED
154
+ * - `**` in the URI is valid only under `?fn=find` (or `?fn=count`)
155
+ * - `*`/`?` (no `**`) is valid under `?fn=ls` or `?fn=find` or `?fn=count`
156
+ * - `?fn=count` accepts any URI shape
157
+ *
158
+ * Numeric params (`limit`, `page`) are coerced; throws on NaN. Unknown
159
+ * standard params throw.
48
160
  */
49
161
  export function parseUrl(url) {
50
162
  const qIdx = url.indexOf("?");
51
- const uri = qIdx < 0 ? url : url.slice(0, qIdx);
163
+ const uriRaw = qIdx < 0 ? url : url.slice(0, qIdx);
52
164
  const query = qIdx < 0 ? "" : url.slice(qIdx + 1);
53
- const schemeIdx = uri.indexOf("://");
165
+ const schemeIdx = uriRaw.indexOf("://");
54
166
  let protocol = "";
55
167
  let hostname = "";
56
168
  let path = "";
57
169
  if (schemeIdx >= 0) {
58
- protocol = uri.slice(0, schemeIdx);
59
- const rest = uri.slice(schemeIdx + 3);
170
+ protocol = uriRaw.slice(0, schemeIdx);
171
+ const rest = uriRaw.slice(schemeIdx + 3);
60
172
  const slash = rest.indexOf("/");
61
173
  if (slash < 0) {
62
174
  hostname = rest;
@@ -68,13 +180,13 @@ export function parseUrl(url) {
68
180
  }
69
181
  }
70
182
  else {
71
- path = uri;
183
+ path = uriRaw;
72
184
  }
73
185
  const program = protocol ? `${protocol}://${hostname}` : "";
186
+ // Split off any glob tail embedded in the path.
187
+ const { prefix: uriPrefix, glob: uriGlob } = splitLocatorGlob(uriRaw);
74
188
  const sp = new URLSearchParams(query);
75
189
  const explicitFn = sp.get("fn") ?? undefined;
76
- const defaultFn = uri.endsWith("/") ? "ls" : "read";
77
- const fn = explicitFn ?? defaultFn;
78
190
  const params = {};
79
191
  const ext = {};
80
192
  for (const [key, value] of sp.entries()) {
@@ -97,15 +209,87 @@ export function parseUrl(url) {
97
209
  case "cursor":
98
210
  case "sortBy":
99
211
  case "sortOrder":
100
- case "pattern":
101
212
  case "format":
102
213
  params[key] = value;
103
214
  break;
215
+ case "pattern":
216
+ params.pattern = value;
217
+ break;
218
+ case "fields":
219
+ params.fields = value.split(",").map((f) => f.trim()).filter(Boolean);
220
+ break;
104
221
  default:
105
222
  throw new Error(`Unknown read param: ${key}`);
106
223
  }
107
224
  }
108
- return { protocol, hostname, path, program, uri, fn, params, ext };
225
+ // Reconcile URI-embedded glob with legacy `?pattern=`.
226
+ // Both populated → conflict → throw (caller has two intents in one URL).
227
+ // Only `?pattern=` populated → dual-accept transition; log deprecation;
228
+ // leave `parsed.uri` as the literal uriRaw (it has no wildcards) and
229
+ // `parsed.glob` carries the pattern value (the routing identity is
230
+ // still the literal uri).
231
+ // Only URI glob populated → preferred path; mirror into params.pattern
232
+ // so legacy push-down sites that read `params.pattern` keep working.
233
+ let effectiveGlob = uriGlob;
234
+ let effectiveUri = uriPrefix;
235
+ if (uriGlob && params.pattern !== undefined) {
236
+ throw new Error(`b3nd-save: cannot combine URI-embedded glob ("${uriGlob}") with ` +
237
+ `?pattern= query param ("${params.pattern}") — pick one`);
238
+ }
239
+ if (!uriGlob && params.pattern !== undefined) {
240
+ logPatternDeprecationOnce();
241
+ effectiveGlob = params.pattern;
242
+ effectiveUri = uriRaw;
243
+ }
244
+ if (uriGlob && params.pattern === undefined) {
245
+ params.pattern = uriGlob;
246
+ }
247
+ // Verb resolution. The default lookup uses the literal-uri shape
248
+ // (trailing slash → ls else read) but is only honoured when there
249
+ // are no wildcards anywhere.
250
+ const hasWildcards = effectiveGlob.length > 0;
251
+ let fn;
252
+ if (explicitFn !== undefined) {
253
+ fn = explicitFn;
254
+ }
255
+ else if (hasWildcards) {
256
+ throw new Error(`b3nd-save: URL contains wildcards but ?fn= is absent — declare ` +
257
+ `the verb explicitly (e.g. ?fn=find for "**", ?fn=ls for "*"/"?")`);
258
+ }
259
+ else {
260
+ fn = uriRaw.endsWith("/") ? "ls" : "read";
261
+ }
262
+ // Verb/shape validation when wildcards are present (§3.4).
263
+ if (hasWildcards) {
264
+ const hasDoubleStar = effectiveGlob.includes("**");
265
+ if (fn === "ls" && hasDoubleStar) {
266
+ throw new Error(`b3nd-save: ?fn=ls rejects URLs containing "**" — use ?fn=find for ` +
267
+ `recursive walks`);
268
+ }
269
+ if (fn === "find" && !hasDoubleStar) {
270
+ throw new Error(`b3nd-save: ?fn=find requires "**" in the URL (or in legacy ` +
271
+ `?pattern=); got "${effectiveGlob}"`);
272
+ }
273
+ // ?fn=count accepts both shapes per §3.2; x-* extensions are
274
+ // provider-defined and pass through; ?fn=read with wildcards is
275
+ // nonsense and we reject it explicitly so callers don't get a
276
+ // confusing point-read attempt on a literal "**" key.
277
+ if (fn === "read") {
278
+ throw new Error(`b3nd-save: ?fn=read rejects URLs containing wildcards — use ?fn=ls ` +
279
+ `or ?fn=find`);
280
+ }
281
+ }
282
+ return {
283
+ protocol,
284
+ hostname,
285
+ path,
286
+ program,
287
+ uri: effectiveUri,
288
+ glob: effectiveGlob,
289
+ fn,
290
+ params,
291
+ ext,
292
+ };
109
293
  }
110
294
  /**
111
295
  * Serialize a parsed url back to its string form. Authoritative input is
@@ -114,6 +298,12 @@ export function parseUrl(url) {
114
298
  *
115
299
  * Omits `fn=` when it matches the trailing-slash default. Throws if any
116
300
  * `ext` key does not start with `x-`.
301
+ *
302
+ * Note: `buildUrl` writes the URI verbatim — it does not splice a glob
303
+ * back into the path. Callers that have a `ParsedUrl` produced by
304
+ * `parseUrl` from a URI-embedded-glob source should reconstruct the
305
+ * full url manually if they need to round-trip the original string; the
306
+ * cheap path is `<originalUrl>` which they already have.
117
307
  */
118
308
  export function buildUrl(parsed) {
119
309
  const { uri, fn, params, ext } = parsed;
@@ -144,6 +334,10 @@ export function buildUrl(parsed) {
144
334
  * request-time-only and never participates in routing or storage
145
335
  * keying. Cheap helper for stores and callers that need the bare uri
146
336
  * without paying for full parse.
337
+ *
338
+ * Note: this returns the URI as written, **including** any embedded
339
+ * glob (`<uri-with-glob>`). For just the literal routing prefix, parse
340
+ * the url and read `parsed.uri`.
147
341
  */
148
342
  export function uriOf(url) {
149
343
  const qIdx = url.indexOf("?");
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @module
3
+ * `walkViaLs` — caller-invoked polyfill that simulates `fn=find`
4
+ * against stores that advertise only `fn=ls`.
5
+ *
6
+ * **When to use.** A caller wants every entry under a prefix (the
7
+ * recursive-listing use case `fn=find` solves) but is targeting a store
8
+ * whose `status().fns` includes `"ls"` and NOT `"find"`. Without this
9
+ * helper the caller would have to hand-roll the recursive walk.
10
+ *
11
+ * **When NOT to use.** If the target store advertises `"find"`, call
12
+ * `?fn=find` directly — it will be cheaper (push-down where possible,
13
+ * single network round-trip in many cases). `walkViaLs` is the explicit
14
+ * fallback, *never* an auto-engaged dispatch fallback (per v1 spec §7
15
+ * `.cc-chat/20260625093437-listing-spec/output.md`). Capability check
16
+ * is the caller's responsibility:
17
+ *
18
+ * ```ts
19
+ * const status = await rig.status();
20
+ * const fns = status?.fns ?? [];
21
+ * const uris = fns.includes("find")
22
+ * ? await rig.read([`${prefix}**?fn=find&format=uris`])
23
+ * : await walkViaLs(rig.read.bind(rig), prefix, { format: "uris" });
24
+ * ```
25
+ *
26
+ * **Contract.** `walkViaLs` uses ONLY the standard b3nd PIN `read`
27
+ * surface — no privileged store access, no rig internals. It issues
28
+ * repeated shallow `?fn=ls&format=uris` calls level-by-level, recursing
29
+ * into each child whose own `?fn=ls&format=uris` returns at least one
30
+ * entry (probe-based directory detection — see Design notes below).
31
+ *
32
+ * **What this DOES NOT do:**
33
+ * - No streaming. The full result accumulates before returning.
34
+ * - No pagination. The helper always returns the complete walk; if
35
+ * the underlying store's `ls` paginates, callers must wrap that
36
+ * concern themselves (or push for `fn=find` support).
37
+ * - No parallelism. Each level is walked serially. This is a
38
+ * polyfill; the right answer for performance is `fn=find`.
39
+ * - No content-equivalence guarantees with `fn=find`. Push-down
40
+ * `fn=find` may sort/paginate differently. `walkViaLs` returns a
41
+ * stable depth-first traversal in the order each level's `ls`
42
+ * yields its children.
43
+ *
44
+ * **Cost model.** O(D) sequential round-trips where D is the count of
45
+ * non-empty directories under `prefix`. For deep trees this is much
46
+ * worse than a single `fn=find` push-down. Use only when capability
47
+ * forces it.
48
+ *
49
+ * **Design notes** (decisions documented for reviewers):
50
+ *
51
+ * 1. *Directory detection: probe via ls, not URI shape.* A naive
52
+ * "no trailing slash → leaf" or "no dot-extension → directory"
53
+ * heuristic would mis-classify common shapes (an entity named
54
+ * `notes` with no extension; a file URI ending in `/`). Probing
55
+ * via `?fn=ls&format=uris` is authoritative: if it returns any
56
+ * URIs, recurse; if it returns the empty set, treat as terminal.
57
+ * The cost is one extra ls per leaf URI — accepted because
58
+ * `walkViaLs` is already O(D) and the spec calls it out as a slow
59
+ * polyfill.
60
+ *
61
+ * 2. *Pattern filtering happens at the helper, not the wire.* The
62
+ * underlying store's `ls` only supports `*` / `?` (no `**` — that's
63
+ * why we're walking via ls). `compileSaveGlob` from `./glob.ts`
64
+ * applies the full v2 §3.3 grammar (including `**`) to the
65
+ * accumulated tail-URIs before returning. Callers see the same
66
+ * glob semantics they would from `fn=find`.
67
+ *
68
+ * 3. *No cursor pagination.* `fn=find` ships a trailing cursor slot
69
+ * (v2 §3.5); this polyfill always returns the full accumulated set
70
+ * and never appends a cursor slot. Callers needing pagination must
71
+ * upgrade to `fn=find`-capable storage.
72
+ */
73
+ import type { Output } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.js";
74
+ /**
75
+ * Options for {@link walkViaLs}. Mirrors the subset of `find` params
76
+ * the helper can honor against a `ls`-only store.
77
+ */
78
+ export interface WalkViaLsOptions {
79
+ /**
80
+ * Glob pattern applied to URI tails (relative to `prefix`). Uses the
81
+ * v2 §3.3 grammar — `*`, `?`, and `**` all work. Patterns are matched
82
+ * against the post-prefix tail (same anchoring as `matchesGlob`).
83
+ * Defaults to "all descendants" (no pattern filter).
84
+ */
85
+ pattern?: string;
86
+ /**
87
+ * `"full"` (default) returns `Output[]` — each `[uri, payload]` is
88
+ * the result of a separate `?fn=read` on that leaf. `"uris"` returns
89
+ * `string[]` — leaf URIs only, no payload reads.
90
+ */
91
+ format?: "full" | "uris";
92
+ /**
93
+ * Soft cap on the number of distinct directories the helper will
94
+ * probe. Defaults to 10_000. When exceeded, the helper throws — the
95
+ * caller is almost certainly walking a larger tree than they meant
96
+ * to (or hitting a backend cycle). Set explicitly to opt into deeper
97
+ * walks.
98
+ */
99
+ maxDirs?: number;
100
+ }
101
+ /**
102
+ * Caller-invoked recursive `ls` walker — the polyfill for stores that
103
+ * advertise only `fn=ls`.
104
+ *
105
+ * @param read The b3nd `read` surface (`rig.read`, a connection's
106
+ * `read`, or any function with the same signature). Every
107
+ * call passes a single-element URL array; the helper never
108
+ * batches.
109
+ * @param prefix The routing prefix to walk. Trailing `/` is normalized
110
+ * in — callers can pass either form.
111
+ * @param opts See {@link WalkViaLsOptions}.
112
+ *
113
+ * @returns `Output[]` for `format=full` (default) or `string[]` for
114
+ * `format=uris`. The ordering is depth-first, in the order each
115
+ * probed directory's `ls` yields its children.
116
+ *
117
+ * @throws If a probed directory's `ls` rejects, the rejection
118
+ * propagates. If the per-walk directory cap is exceeded, throws
119
+ * `Error("walkViaLs: maxDirs exceeded")`.
120
+ */
121
+ export declare function walkViaLs(read: <T = unknown>(urls: string[]) => Promise<Output<T>[]>, prefix: string, opts?: WalkViaLsOptions): Promise<Output[] | string[]>;
122
+ //# sourceMappingURL=walk-via-ls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walk-via-ls.d.ts","sourceRoot":"","sources":["../src/walk-via-ls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kEAAkE,CAAC;AAG/F;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAC3D,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CA+E9B"}