@spree/docs 0.1.193 → 0.1.195

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.
@@ -20,10 +20,8 @@ flowchart LR
20
20
  erDiagram
21
21
  Catalog ||--o{ CatalogProduct : "assortment"
22
22
  Catalog ||--o{ CatalogAssignment : "audiences"
23
- Catalog }o--o| PriceList : "optional pricing"
24
- CatalogAssignment }o--|| Channel : "one of"
23
+ Catalog ||--o| PriceList : "owns (optional)"
25
24
  CatalogAssignment }o--|| CustomerGroup : "one of"
26
- CatalogAssignment }o--|| Market : "one of"
27
25
  CatalogAssignment }o--|| Company : "one of"
28
26
 
29
27
  Catalog {
@@ -60,6 +58,8 @@ const catalog = await adminClient.catalogs.create({
60
58
 
61
59
  The price list is optional. A catalog with an assortment and no price list restricts the range at normal prices; a catalog with a price list and no assortment adjusts prices without hiding anything.
62
60
 
61
+ A price list is either **standalone** (matched by its own rules) or **owned by exactly one catalog** — attaching a list a catalog already claims moves it. Detaching (`price_list_id: null`) is an explicit act with a real consequence: a released list starts matching by its own rules again, and a rule-less one matches everyone.
62
+
63
63
  ### Filling the assortment
64
64
 
65
65
  ```typescript Admin SDK
@@ -71,7 +71,7 @@ const { data: products } = await adminClient.catalogs.products.list(catalog.id)
71
71
  await adminClient.catalogs.products.delete(catalog.id, 'prod_xxx')
72
72
  ```
73
73
 
74
- Products are positioned, so a catalog also controls the order they're presented in.
74
+ Membership is all a catalog holds it decides *what* a buyer sees, never the order they see it in. Presentation order stays with categories and collections.
75
75
 
76
76
  When a catalog should restrict to exactly what its price list covers, there's a shortcut that copies those products in rather than making someone add them by hand:
77
77
 
@@ -81,15 +81,15 @@ const { added_count } = await adminClient.catalogs.importProducts(catalog.id)
81
81
 
82
82
  ## Choosing the audience
83
83
 
84
- A catalog is assigned to one of four things:
84
+ A catalog is assigned to a buyer audience:
85
85
 
86
86
  | Assign to | Meaning |
87
87
  |---|---|
88
- | **[Channel](channels.md)** | Everyone buying through this channel — an app, a retail till |
89
88
  | **[Customer group](customers.md)** | A segment — wholesale accounts, staff, a loyalty tier |
90
- | **[Market](markets.md)** | Everyone in a region |
91
89
  | **[Company](companies.md)** | One B2B organization |
92
90
 
91
+ A [Channel](channels.md) is not assigned — it names its catalog directly via `default_catalog_id`, which applies to everyone buying through it when nothing narrower does.
92
+
93
93
  ```typescript Admin SDK
94
94
  await adminClient.catalogs.assign(catalog.id, {
95
95
  assignable_type: 'customer_group',
@@ -128,9 +128,9 @@ Prices are checked in order, and the first match wins:
128
128
 
129
129
  Nearest-first is what lets a subsidiary hold a better-negotiated rate than the group's, without disturbing anyone else.
130
130
 
131
- > **WARNING:** A price list attached to a catalog applies **because the catalog applies** — its own rules are not consulted, and it's excluded from ordinary rule matching.
131
+ > **WARNING:** A price list owned by a catalog applies **because the catalog applies** — its own rules are not consulted, and it's excluded from ordinary rule matching.
132
132
  >
133
- > That exclusion is load-bearing: a price list with no rules would otherwise match everyone, and one company's negotiated pricing would leak to the entire storefront.
133
+ > That exclusion is load-bearing: a price list with no rules would otherwise match everyone, and one company's negotiated pricing would leak to the entire storefront. It also means a deactivated catalog's list goes **dormant** — turning the catalog off never releases its pricing to the whole store. Only explicitly detaching the list puts it back into rule matching.
134
134
 
135
135
  ## Related
136
136
 
@@ -142,7 +142,7 @@ const priceList = await client.priceLists.create({
142
142
  name: 'EU wholesale',
143
143
  match_policy: 'all',
144
144
  rules: [
145
- { type: 'customer_group_rule', preferences: { customer_group_ids: ['cg_xxx'] } },
145
+ { type: 'market_rule', preferences: { market_ids: ['mkt_xxx'] } },
146
146
  ],
147
147
  prices: [
148
148
  { variant_id: 'variant_xxx', currency: 'EUR', amount: '19.99' },
@@ -165,16 +165,18 @@ spree api patch /price_lists/pl_xxx/activate
165
165
 
166
166
  ## Price Rules
167
167
 
168
- Price Rules define conditions that must be met for a Price List to apply. Spree includes five built-in rule types:
168
+ Price Rules define conditions that must be met for a Price List to apply. Rules describe the *context* of a purchase — where, through what, how much:
169
169
 
170
170
  | Rule | Description | Use Case |
171
171
  |------|-------------|----------|
172
172
  | **Market Rule** | Matches based on the current [Market](markets.md) | Regional pricing across markets |
173
173
  | **Channel Rule** | Matches based on the [Channel](channels.md) the customer is buying through | App-only or in-store pricing |
174
- | **User Rule** | Matches specific customer accounts | VIP customers, wholesale accounts |
175
- | **Customer Group Rule** | Matches members of customer groups | Loyalty tiers, membership pricing |
176
174
  | **Volume Rule** | Matches based on quantity purchased | Bulk discounts, tiered pricing |
177
175
 
176
+ *Who* gets a price list is not a rule — it's a [Catalog](catalogs.md): attach the list to a catalog assigned to the customer groups or companies it's for.
177
+
178
+ > **NOTE:** The **User Rule** and **Customer Group Rule** kinds still work and are not going away, but they're no longer offered when adding a rule — targeting an audience through a catalog is the one way to do it now. Nothing needs migrating; rules you already have keep matching exactly as before.
179
+
178
180
  ### Market Rule
179
181
 
180
182
  Regional pricing. Applies the Price List when the customer is in one of the specified markets.
@@ -183,15 +185,7 @@ Regional pricing. Applies the Price List when the customer is in one of the spec
183
185
 
184
186
  ### Channel Rule
185
187
 
186
- Applies when the customer is buying through a particular sales channel — a discount that exists only in the mobile app, or a price that only applies at a retail till.
187
-
188
- ### User Rule
189
-
190
- Limits the Price List to specific customer accounts. Useful for VIP customers, employee pricing, or wholesale accounts.
191
-
192
- ### Customer Group Rule
193
-
194
- Applies to members of specific customer groups. Useful for wholesale tiers, loyalty programs, or membership-based pricing.
188
+ Applies when the customer is buying through a particular sales channel — a discount that exists only in the mobile app, or a price that only applies at a retail till. For new setups, prefer giving the channel a default [Catalog](catalogs.md) that owns the price list — that is where channel-wide commercial terms live going forward.
195
189
 
196
190
  ### Volume Rule
197
191
 
@@ -214,7 +208,7 @@ When resolving prices, Spree considers the full context of the request:
214
208
  | Currency | Market or request header | The currency to price in |
215
209
  | Market | Customer's country | The [Market](markets.md) for market-based rules |
216
210
  | Channel | The API credential in use | The [Channel](channels.md) for channel-based rules |
217
- | Customer | Authentication | The signed-in customer for user-based rules |
211
+ | Customer | Authentication | The signed-in customer, for resolving their [Catalogs](catalogs.md) |
218
212
  | Quantity | Cart line item | The quantity for volume-based rules |
219
213
  | Date | Current time | For time-based Price List scheduling |
220
214
 
@@ -716,6 +716,115 @@ The API automatically returns the correct price based on the current currency an
716
716
 
717
717
  See the [Pricing](pricing.md) guide for details on Price Lists, Price Rules, and market-specific pricing.
718
718
 
719
+ ## Digital products
720
+
721
+ **A digital product is an ordinary product whose variant delivers without shipping — its [delivery profile](fulfillments.md) is a digital one, so buying it grants the customer a download instead of dispatching a parcel.** Nothing about the catalog model changes — you still have a product, its variants, and its prices. What that variant hands over is usually a **digital asset** it carries (an e‑book, a design file) or a value a provider mints on demand (a license key) — but the file is the optional deliverable, not what makes the product digital.
722
+
723
+ Two ideas are worth separating up front, because they are independent:
724
+
725
+ - **Being digital** is a delivery decision. A variant is digital when its [delivery profile](fulfillments.md) is a digital one — that variant needs no shipping address, and a cart made up entirely of digital variants skips the delivery step at checkout.
726
+ - **Carrying downloadable files** is a catalog decision. Any variant can own digital assets — including a physical one, so a boxed product can ship a warranty PDF or a setup guide alongside the goods.
727
+
728
+ Most digital products are both: a digital variant that carries the files it delivers. But the two are decoupled on purpose, so "ships nothing" and "hands over a file" can be mixed as a merchant needs.
729
+
730
+ ```mermaid
731
+ erDiagram
732
+ Product ||--|{ Variant : "has"
733
+ Variant ||--o{ DigitalAsset : "carries"
734
+ DigitalAsset ||--o{ DigitalLink : "granted as"
735
+ LineItem ||--o{ DigitalLink : "purchased in"
736
+ DigitalAsset {
737
+ string provider_type "blank = uploaded file"
738
+ int authorized_clicks "nullable, falls back to store"
739
+ int authorized_days "nullable, falls back to store"
740
+ }
741
+ DigitalLink {
742
+ string token "the download credential"
743
+ int access_counter "downloads spent"
744
+ }
745
+ ```
746
+
747
+ ### Assets live on the variant
748
+
749
+ A digital asset (`Spree::DigitalAsset`) belongs to a variant and holds either an uploaded file or a reference to a provider that produces the deliverable on demand (see [Where the file comes from](#where-the-file-comes-from) below). Uploaded files go to **private storage** — they are only ever served through a short‑lived, signed link, never a public URL.
750
+
751
+ You attach assets to a variant from the product's **Digital files** card in the dashboard, or through the Admin API nested under the product:
752
+
753
+
754
+ ```typescript Admin SDK
755
+ // 1. Ask for a private-storage upload slot, then PUT the file to the returned URL.
756
+ const { direct_upload, signed_id } = await adminClient.directUploads.create({
757
+ private: true, // digital files are only ever served through a signed link
758
+ blob: { filename: 'guide.pdf', byte_size: file.size, checksum, content_type: 'application/pdf' },
759
+ })
760
+ await fetch(direct_upload.url, { method: 'PUT', headers: direct_upload.headers, body: file })
761
+
762
+ // 2. Attach the uploaded blob to the product's default variant.
763
+ await adminClient.products.digitalAssets.create('prod_86Rf07xd4z', {
764
+ signed_id,
765
+ authorized_clicks: 5, // optional — omit to inherit the store default
766
+ authorized_days: 30,
767
+ })
768
+ ```
769
+
770
+ ```bash cURL
771
+ curl -X POST 'https://api.mystore.com/api/v3/admin/products/prod_86Rf07xd4z/digital_assets' \
772
+ -H 'X-Spree-API-Key: sk_xxx' \
773
+ -H 'Content-Type: application/json' \
774
+ -d '{ "signed_id": "eyJfcmFpbHMi...", "authorized_clicks": 5, "authorized_days": 30 }'
775
+ ```
776
+
777
+
778
+ > **WARNING:** Digital files must be uploaded to **private storage**. The Admin API refuses a blob that landed on the public service — attaching never moves a file between services, so a public upload would stay publicly readable while looking attached. Request the direct upload with `private: true`.
779
+
780
+ Replacing an asset's file keeps every download link that was already issued working: links resolve through the asset, not the underlying blob, so a merchant can swap a corrected file mid‑sale without breaking anyone's access.
781
+
782
+ ### A purchase grants download links
783
+
784
+ When an order is placed, digital assets fulfill themselves. The digital fulfillment provider runs automatically (it needs no address and no manual action) and, for each purchased unit, creates one **download link** (`Spree::DigitalLink`) per asset on the variant. Buy three copies of a two‑file bundle and the buyer gets six links. Re‑running fulfillment is idempotent — it never duplicates links.
785
+
786
+ A download link is the customer's **grant**: a globally unique token, a counter of downloads spent, and its own copy of the allowance. The token *is* the credential — it identifies the store on its own, which is why an emailed link works without any API key.
787
+
788
+ Customers reach their files two ways:
789
+
790
+ - **By email.** On order placement, `Spree::DigitalAssetMailer` sends a "your files are ready" message with the links — a message of its own, separate from the order confirmation, so it can be re‑sent from the order page later.
791
+ - **From their account.** A signed‑in customer sees every link they have ever been granted, across all their orders, through the customer downloads endpoint.
792
+
793
+ > **NOTE:** The files-ready email is only sent when the order actually has digital links and the store has consumer transactional emails enabled. A store that delivers files through its own storefront or webhooks can leave it off.
794
+
795
+ ### Downloading, and the allowance
796
+
797
+ A download is a `GET` against the link's token. Every download runs the same guarded sequence, and the customer's allowance is only spent once a deliverable is actually in hand:
798
+
799
+ 1. **The grant must be live** — attempts remaining, and not past its expiry.
800
+ 2. **The signed‑URL window must be open** — clamped to whatever is shorter, the store's link lifetime or the link's own remaining days.
801
+ 3. **The deliverable is produced** — see providers below. This step is allowed to fail.
802
+ 4. **The download is charged** — the counter is incremented under a lock.
803
+ 5. **The file is handed over** — a redirect to a signed URL, or an inline body.
804
+
805
+ The ordering is deliberate: producing the deliverable comes *before* charging the click, so a provider outage or a missing file returns an honest error and **costs the customer nothing**. Two allowances govern access, both falling back to store settings when the asset leaves them blank:
806
+
807
+ | Field | Meaning | Store default |
808
+ | --- | --- | :---: |
809
+ | `authorized_clicks` | How many times the file may be downloaded | `5` |
810
+ | `authorized_days` | How long after purchase the link stays valid | `7` |
811
+
812
+ Store‑wide defaults and their on/off switches live at **Settings → Store**; the separate `digital_asset_link_expire_time` preference caps how long a single signed URL lives (default 5 minutes, never more than an hour) because that URL is a bearer credential. When a customer runs out of downloads or a file was replaced mid‑flight, an admin can restore access by resetting the link from the order page, which zeroes the counter and restarts the clock.
813
+
814
+ Each successful download publishes an event you can subscribe to:
815
+
816
+ | Event | Published when |
817
+ | --- | --- |
818
+ | `digital_link.downloaded` | A customer successfully downloads a file (after the click is charged) |
819
+
820
+ ### Where the file comes from
821
+
822
+ By default a digital asset delivers its uploaded file. But the last step — "hand something over" — is pluggable through a **digital asset provider**, so the deliverable can instead be minted on demand: a license key from your billing system, an entitlement from internal software, or a signed link to a file on your own host. The purchase, the grant, the allowance, and the email are identical either way; only the production of the deliverable changes.
823
+
824
+ A blank `provider_type` on an asset means the built‑in file provider — the uploaded‑file behavior described above. Registering your own provider adds it as a source on the Digital files card, so a merchant can pick it when adding an asset.
825
+
826
+ > **TIP:** To build one, see the [Build a Custom Digital Asset Provider](../how-to/custom-digital-asset-provider.md) how‑to. It covers the `#deliver` contract, per‑asset settings, and registration.
827
+
719
828
  ## Categories
720
829
 
721
830
  There are two ways to group products, and they answer different questions.
@@ -934,6 +1043,7 @@ See [Sales Channels](channels.md) for the full channel lifecycle, including defa
934
1043
  - [Pricing](pricing.md) — Price Lists, Price Rules, and market-specific pricing
935
1044
  - [Inventory](inventory.md) — Stock management and backorders
936
1045
  - [Media](media.md) — Image management
1046
+ - [Build a Custom Digital Asset Provider](../how-to/custom-digital-asset-provider.md) — Deliver a license key or external file instead of an uploaded one
937
1047
  - [Translations](translations.md) — Translating product content
938
1048
  - [Search & Filtering](search-filtering.md) — Full-text search and Ransack filtering
939
1049
  - [Store SDK Products](../sdk/store/products.md) — Listing, fetching, filtering, and categories via `client.products`
@@ -76,7 +76,7 @@ setting :region, :select, in: %w[us eu], default: 'us'
76
76
  setting :auto_revoke, :boolean, default: false
77
77
  ```
78
78
 
79
- Field types are `:string`, `:number`, `:boolean`, and `:select` (pass `in:` for the choices). The merchant's answers are stored on the asset and read back through `digital_asset.provider_settings`, a plain hash keyed by the setting name:
79
+ Field types are `:string`, `:number`, `:boolean`, and `:select` (pass `in:` for the choices). Your declarations become the provider's `settings_schema`, which the admin `providers` endpoint exposes so the dashboard can render the form. The merchant's answers are stored on the asset — under one key, `metadata['provider']`, so they never collide with other developer metadata — and read back through `digital_asset.provider_settings`, a plain hash keyed by the setting name:
80
80
 
81
81
  ```ruby
82
82
  digital_asset.provider_settings['pool_name'] # => "winter-sale"
@@ -94,6 +94,8 @@ end
94
94
 
95
95
  Once registered, the provider appears as a source on the product's **Digital files** card: the **Add** button becomes a menu offering **Upload a file** alongside each registered provider. Picking your provider creates an asset with its `provider_type` set and no file attached.
96
96
 
97
+ > **NOTE:** Registration is what makes a `provider_type` valid. An asset validates that its `provider_type` names a registered provider, so an unregistered or misspelled class name is rejected with a 422 rather than failing at download time. A blank `provider_type` is always the built-in `File` provider.
98
+
97
99
  ## The download contract
98
100
 
99
101
  Every download follows the same order, and a provider only participates in the last step:
@@ -130,4 +132,5 @@ Spree::DigitalDelivery.new(inline_value: 'ABCD-1234-EFGH', content_type: 'text/p
130
132
  ## Related documentation
131
133
 
132
134
  - [Digital products](/use-case/digital-products/capabilities) — what the feature does for merchants
133
- - [Build a Custom Delivery Rate Provider](/v6/developer/how-to/custom-delivery-rate-provider) — a provider strategy that *does* use an integration, for contrast
135
+ - [Products](../core-concepts/products.md#digital-products) — how digital assets, links, and downloads fit together
136
+ - [Build a Custom Delivery Rate Provider](custom-delivery-rate-provider.md) — a provider strategy that *does* use an integration, for contrast
@@ -3,18 +3,75 @@ title: Sell Digital Products
3
3
  description: Digital goods in Spree — a digital delivery profile, instant fulfillment without a shipping address, and delivering files or entitlements after payment.
4
4
  ---
5
5
 
6
- Digital products skip the warehouse: no stock to reserve, no address to collect, no carrier to call. In Spree 6 that behavior comes from the product's delivery profile — assign a digital profile and checkout, fulfillment and the customer experience adapt.
6
+ Digital products skip the warehouse: no stock to reserve, no address to collect, no carrier to call. In Spree 6 that behavior comes from the product's delivery profile — assign a digital profile and checkout, fulfillment and the customer experience adapt. This guide walks through selling an uploaded file end to end.
7
+
8
+ For the concepts behind each piece — assets, links, allowances, providers — see [Products → Digital products](../core-concepts/products.md#digital-products). This page is the practical walkthrough.
7
9
 
8
10
  ## What you will build
9
11
 
10
- - **Digital delivery profile** — products that require no shipping address and complete checkout without a delivery step.
11
- - **Instant fulfillment** — the digital fulfillment provider fulfills on payment rather than on dispatch.
12
- - **Delivery of the goods** — attaching files or granting entitlements, and notifying the customer.
13
- - **Mixed carts** — physical and digital items in one order, split into separate fulfillments automatically.
12
+ - **A digital variant** — one whose delivery profile requires no shipping address, so a fully digital cart completes checkout without a delivery step.
13
+ - **A downloadable file on it** — uploaded to private storage and attached as a digital asset.
14
+ - **Automatic delivery** — a download link granted on order placement, and an email that sends it.
15
+ - **Mixed carts** — physical and digital items in one order, handled without extra work.
16
+
17
+ ## 1. Make the product digital
18
+
19
+ A variant is digital when its [delivery profile](../core-concepts/fulfillments.md) is a digital one. A store ships with a built-in **Digital** delivery profile; assign it to the product and the product's variants stop asking for a shipping address.
20
+
21
+
22
+ ```typescript Admin SDK
23
+ // Assign the store's digital delivery profile to the product.
24
+ const { data: profiles } = await adminClient.deliveryProfiles.list()
25
+ const digital = profiles.find((p) => p.kind === 'digital')
26
+
27
+ await adminClient.products.update('prod_86Rf07xd4z', {
28
+ delivery_profile_id: digital.id,
29
+ })
30
+ ```
31
+
32
+ ```bash cURL
33
+ curl -X PATCH 'https://api.mystore.com/api/v3/admin/products/prod_86Rf07xd4z' \
34
+ -H 'X-Spree-API-Key: sk_xxx' \
35
+ -H 'Content-Type: application/json' \
36
+ -d '{ "delivery_profile_id": "dp_digital" }'
37
+ ```
38
+
39
+
40
+ > **NOTE:** A product's delivery profile is auto-assigned when it is created — from its product type's template, or the store default. You only set it explicitly to change it. Selling from the dashboard, this is the **Shipping profile** field on the product page.
41
+
42
+ ## 2. Attach the file
43
+
44
+ Upload the file to **private storage** and attach it to the variant as a digital asset. Private storage matters: a digital file is only ever served through a short-lived signed link, so the Admin API refuses a blob that landed on the public bucket.
45
+
46
+ ```typescript Admin SDK
47
+ // Request a private upload slot, PUT the file, then attach the blob.
48
+ const { direct_upload, signed_id } = await adminClient.directUploads.create({
49
+ private: true,
50
+ blob: { filename: 'guide.pdf', byte_size: file.size, checksum, content_type: 'application/pdf' },
51
+ })
52
+ await fetch(direct_upload.url, { method: 'PUT', headers: direct_upload.headers, body: file })
53
+
54
+ await adminClient.products.digitalAssets.create('prod_86Rf07xd4z', { signed_id })
55
+ ```
56
+
57
+ Leave `authorized_clicks` and `authorized_days` off to inherit the store's download limits, or set them per file to mix evergreen downloads with time-limited ones. See [the allowance](../core-concepts/products.md#downloading-and-the-allowance) for how the limits resolve.
58
+
59
+ > **TIP:** You can attach a file to a **physical** variant too — a manual or warranty PDF that ships alongside the goods. Carrying files and being digital are independent: the physical variant still needs an address, and it also grants a download link.
60
+
61
+ ## 3. Let the order deliver itself
62
+
63
+ There is no step three to configure. When the order is placed, the digital fulfillment provider runs automatically — it needs no address and no manual dispatch — and grants the buyer one download link per file, per purchased unit. `Spree::DigitalAssetMailer` then emails the links as a message of its own, so it survives a replaced order-confirmation template and can be re-sent from the order page.
64
+
65
+ The customer downloads by following the link's token; each download is counted against the allowance and publishes a `digital_link.downloaded` event. If they exhaust their downloads or you replace the file mid-sale, an admin restores access by resetting the link from the order page.
66
+
67
+ > **NOTE:** To deliver something other than an uploaded file — a license key minted on demand, an entitlement from your own system — the delivery step is pluggable. See [Build a Custom Digital Asset Provider](custom-digital-asset-provider.md).
68
+
69
+ ## 4. Mixed carts work on their own
70
+
71
+ A cart with both physical and digital items needs nothing special. Spree splits the order into separate fulfillments per delivery profile, so the physical items collect an address and ship while the digital items skip straight to granting links. The delivery step only appears when at least one item actually needs shipping; an all-digital cart never sees it.
14
72
 
15
- ## Outline
73
+ ## Related documentation
16
74
 
17
- 1. Create a digital delivery profile and assign products
18
- 2. Configure the digital fulfillment provider
19
- 3. Deliver files or entitlements on fulfillment
20
- 4. Handle mixed physical and digital orders
75
+ - [Products Digital products](../core-concepts/products.md#digital-products) the concepts: assets, links, allowances, providers
76
+ - [Build a Custom Digital Asset Provider](custom-digital-asset-provider.md) deliver a key or external file instead of an upload
77
+ - [Fulfillments](../core-concepts/fulfillments.md) delivery profiles, providers, and how orders are fulfilled
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spree/docs",
3
- "version": "0.1.193",
3
+ "version": "0.1.195",
4
4
  "description": "Spree Commerce developer documentation for AI agents and local reference",
5
5
  "type": "module",
6
6
  "license": "CC-BY-4.0",