URLs and identities
The path grammar of a compiled public module, the sources an identity names (registries, Git repositories and archive digests), and the rule that lets one relative URL work on every origin.
- Availability: Experimental
- Evidence: Read from source
- Reference
The path
/m/[<registry>/]<package>@<version>/<family>/<subpath>
/m/git/<host>/<owner>/<repo>@<commit>/<family>/<subpath>
/m/digest/<algorithm>-<hex>/<family>/<subpath>https://cdn.beyondjs.com/m/@example/app@1.0.0/modules/core/router?target=browser&format=esm
https://cdn.beyondjs.com- Base origin: the only part you change
/m/- Namespace
@example/app- Package
@1.0.0- Exact version
/modules/- Resource family
core/router- Public module subpath
?target=browser&format=esm- Options
| Part | Rule |
|---|---|
/m/ |
The namespace of the contract. Every resource path starts with it. |
<registry>/ |
Optional. npm is the default and is not written: the unprefixed form is canonical, and /m/npm/… is an accepted alias of the same identity. Any other registry id (lowercase letters, digits, ., _, -) is part of the identity, so two registries never share a key. git and digest are never registry ids. |
<package> |
The package name, scoped (@example/app) or not (library). |
@<version> |
An exact semantic version, optionally with prerelease and build parts: 1.0.0, 2.1.0-beta.1. Ranges and tags are not identities. Whoever resolves a range requests the exact version it selected. |
/<family>/ |
The resource family: /modules/, /styles/, /maps/ or /assets/; see Outputs, styles and assets. |
<subpath> |
The public module subpath inside the package, without the leading ./. A nested subpath keeps its slashes unencoded: core/router. |
The root module
The root public module of a package, the one you import with the package name alone, is written as the reserved segment ~root:
/m/@example/app@1.0.0/modules/~rootURL parsers remove . and empty segments, and ~ cannot appear in a module subpath, so the segment is reversible and cannot collide with a real module.
What is rejected
One identity has exactly one path. These are all 400 IDENTITY_INVALID:
- an encoded slash or backslash (
%2f,%5c), a backslash, or a.or..segment; - a version that is not exact, or a path without
@<version>; - a path without
/<family>/<subpath>; - a package name, subpath, host, commit or digest with characters outside the grammar.
Sources
The path names where a package comes from. Two packages with the same name and version from different sources have different paths, keys, stored sources and outputs.
| Source | Path prefix | Key of the package | What pins it |
|---|---|---|---|
| The npm registry | /m/<package>@<version> |
npm:<package>@<version> |
The exact version |
| Another npm-compatible registry | /m/<registry id>/<package>@<version> |
<registry id>:<package>@<version> |
The exact version at that registry |
| A Git repository | /m/git/<host>/<owner>/<repo>@<commit> |
git:<host>/<owner>/<repo>@<commit> |
A full 40-character commit. A branch or a tag is pinned to its commit when the graph is resolved. |
| An archive URL | /m/digest/<algorithm>-<hex> |
digest:<algorithm>-<hex> |
The content digest, sha256 or sha512. An address without an integrity fragment is pinned by downloading it once when the graph is resolved. |
- A registry id is written by Beyond Packages when it resolves a package, on the CDN and on a development server alike:
registry-<slug>-<32 hexadecimal digits>, a readable slug of the registry address followed by 128 bits of its SHA-256 digest, so no address can be chosen to look like another registry. Read it from the resolution or the graph; never build it. - A Git source is the repository root. Sub-paths of a repository and
git+ssh:are not sources. - A Git or digest identity has no package name in its path, so it has no public specifier: code still imports the package by the name its manifest declares, and the resolution maps that name to the path.
- One name and version per application. Inside one application, one
name@versionmust come from one source: the runtime registers a loaded module by name, version and subpath. Preparing an application that needs the same name and version from two sources fails withUNSUPPORTED_INPUT. Across applications and organizations, sources never collide.
Which sources each service delivers is in the capability table. A service answers 501 SOURCE_UNSUPPORTED for a well-formed identity of a source it does not deliver.
What a consumer imports
A path is how a module is fetched, not how it is imported. Application code imports the public specifier, which never carries a version:
| Form | Example | Used for |
|---|---|---|
| Specifier | @example/app/core/router |
What source code imports |
| Versioned specifier | @example/app@1.0.0/core/router |
The identity the artifact registers in the runtime |
| Key | npm:@example/app@1.0.0/core/router |
Resolution tables and caches; includes the source |
A resolution document maps specifiers to paths, so that application code stays free of versions and origins.
The same relative URL on every origin
The same module is the same relative path and query on every service that implements the contract. A consumer changes only the base origin: the host, and locally the scheme and port.
http://localhost:<port>/m/@example/app@1.0.0/modules/core/router?target=browser&format=esm
https://cdn.beyondjs.com/m/@example/app@1.0.0/modules/core/router?target=browser&format=esmThis is compatibility of requests, responses and errors, not only of naming. What differs between services is deliberate and limited: which outputs each one holds, its access rules and its Cache-Control. None of those differences changes a URL.
A service reached through the routing of a hosted environment can have a path prefix as part of its base origin. The relative part after it is unchanged. A released application is the main case: /_r/<release number> on its host is a base origin of its own; see the release prefix.
Every public answer, errors included, can be read by a page of another origin: it carries Access-Control-Allow-Origin: * and exposes ETag, SourceMap and Link. A page therefore learns why a module failed, not only that it did. A private resource and the refusals ACCESS_REQUIRED and ACCESS_DENIED depend on who asks and do not allow every origin.
Reading and writing paths in code
Do not parse or build these URLs by hand. @beyond-js/artifact-api is the one implementation of the grammar:
import { ModulePath, ResourcePath, Identity } from '@beyond-js/artifact-api';
const identity = ModulePath.parse('/m/@example/app@1.0.0/modules/core/router');
identity.specifier; // '@example/app/core/router'
identity.vspecifier; // '@example/app@1.0.0/core/router'
identity.key; // 'npm:@example/app@1.0.0/core/router'
ModulePath.format(identity); // the canonical path, reversible
// Git and digest sources are read only by a caller that implements them
const fork = ModulePath.parse('/m/git/github.com/example/app@<commit>/modules/core/router', Identity.sources);
fork.source; // 'git'
fork.key; // 'git:github.com/example/app@<commit>/core/router'
fork.specifier; // undefined: the path carries no package name
const resource = ResourcePath.parse('/m/@example/app@1.0.0/styles/core/router');
resource.kind; // 'style'ModulePath.parse throws a ContractError with the code IDENTITY_INVALID, or SOURCE_UNSUPPORTED for a well-formed identity of a source the caller did not list. The package has no runtime dependencies and importing it has no side effects.