GS1-Conformant Resolution Protocol Specification
GSPEC-RESOLVER-002GS1-Conformant Resolution Protocol Specification
Status: Draft Version: 1.0.0 Last Updated: 2026-01-31 Specification Series: GSPEC-RESOLVER-002 GS1 Conformant Resolver Version: 1.2.0 (January 2026)
Table of Contents
- Overview
- Resolution Architecture
- Resolution Algorithm
- HTTP Interface
- Response Types
- Deactivated Product Handling
- Caching Strategy
- Error Handling
- Integration Points
1. Overview
1.1 Purpose
This specification defines the complete resolution protocol for the Galileo GS1-conformant resolver at id.galileoprotocol.io. The resolver bridges physical product identifiers (encoded in QR codes, NFC tags) to digital identities, enabling ESPR-mandated Digital Product Passport access with context-aware routing.
The resolution protocol:
- Connects physical products to their digital twins
- Routes requests to appropriate data based on requester context
- Integrates with on-chain DID registry and off-chain DPP storage
- Supports ESPR tiered stakeholder access requirements
1.2 Conformance
This specification conforms to:
- GS1-Conformant Resolver Standard 1.2.0 (January 2026)
- GS1 Digital Link Standard 1.6.0 (April 2025)
- IETF RFC 9264 (Linkset format)
- W3C DID Core v1.0 (DID resolution)
References:
- GS1 Digital Link URI Specification
- Linkset Schema
- did:galileo Method Specification
- Hybrid Architecture
1.3 Scope
This specification covers:
- Resolution algorithm (8 steps)
- HTTP interface and endpoints
- Response types (redirect, linkset, error)
- Deactivated product handling
- Caching strategy
- Integration with Galileo infrastructure
1.4 ESPR Mandate
The EU Ecodesign for Sustainable Products Regulation (ESPR) 2024/1781 requires:
- Digital Product Passports accessible via data carriers
- Machine-readable data in interoperable formats
- Tiered access for different stakeholders
- Accessibility throughout product lifecycle
This resolution protocol fulfills these requirements by providing context-aware access to product data.
2. Resolution Architecture
2.1 High-Level Flow
+-------------------+
| |
[QR Code/NFC Tag] -----> [GS1 Digital Link URI] |
| |
v |
+------------------+ |
| Galileo Resolver | |
| (id.galileoprotocol.io) |
+------------------+ |
| |
+--------------------+--------------------+
| | |
v v v
+-----------+ +-----------+ +-----------+
| Parse URI | | Detect | | Auth |
| (Step 1-2)| | Context | | Check |
| | | (Step 3) | | (Step 3) |
+-----------+ +-----------+ +-----------+
| | |
+--------------------+--------------------+
|
v
+------------------+
| Build Galileo DID |
| (Step 4) |
+------------------+
|
+---------------+---------------+
| |
v v
+----------------+ +----------------+
| On-Chain | | Off-Chain |
| Registry | | DPP Store |
| (ProductRecord)| | (DID Document) |
| (Step 5) | | (Step 6) |
+----------------+ +----------------+
| |
+---------------+---------------+
|
v
+------------------+
| Build Linkset |
| Response (Step 7)|
+------------------+
|
v
+------------------+
| 307 Redirect or |
| Linkset (Step 8) |
+------------------+
2.2 Components
| Component | Role | Data Source |
|-----------|------|-------------|
| GS1 URI Parser | Parse and validate incoming URIs | Request URL |
| Context Detector | Determine requester role and permissions | JWT, query params, headers |
| DID Builder | Construct did:galileo from GS1 identifiers | Parsed URI components |
| On-Chain Registry | Authoritative product metadata | Blockchain (contentHash, controller, active) |
| Off-Chain Store | Full DID document and DPP content | IPFS/S3/Azure Blob |
| Linkset Builder | Construct RFC 9264 linkset response | DID document services |
2.3 Domain Architecture
| Domain | Purpose | URL |
|--------|---------|-----|
| id.galileoprotocol.io | GS1 Digital Link resolver | Primary entry point for QR/NFC |
| resolver.galileoprotocol.io | Service endpoint host | DPP, events, verification endpoints |
| auth.galileoprotocol.io | Authentication service | JWT issuance, JWKS |
3. Resolution Algorithm
The Galileo resolver implements an 8-step resolution algorithm per GS1-Conformant Resolver Standard 1.2.0 with Galileo-specific extensions.
3.1 Step 1: Parse GS1 Digital Link URI
Input: Raw request URL Output: Parsed URI components or error
interface ParsedGS1URI {
primaryAI: string; // "01", "8006", "8010", "253"
primaryValue: string; // GTIN, ITIP, CPID, or GDTI
qualifiers: {
ai: string;
value: string;
}[];
queryParams: {
linkType?: string;
context?: string;
lang?: string;
};
}
function parseGS1DigitalLink(uri: string): ParsedGS1URI | null {
// Validate domain is id.galileoprotocol.io
// Extract path segments as AI/value pairs
// Parse query parameters
// Reference: digital-link-uri.md Section 2
}
Error Conditions:
- Invalid domain: Return 400
INVALID_DOMAIN - Malformed path: Return 400
INVALID_PATH - Missing primary identifier: Return 400
MISSING_IDENTIFIER
3.2 Step 2: Validate Identifiers
Input: Parsed URI components Output: Validated identifiers or error
Validation Rules:
| Identifier | Validation |
|------------|------------|
| GTIN (AI 01) | 14 digits, valid Modulo-10 check digit |
| Serial (AI 21) | 1-20 alphanumeric chars, pattern [A-Za-z0-9\-\.]+ |
| ITIP (AI 8006) | 18 digits (GTIN-14 + piece/total) |
| CPID (AI 8010) | 1-30 alphanumeric chars |
| GDTI (AI 253) | 13-30 digits |
function validateIdentifiers(parsed: ParsedGS1URI): ValidationResult {
// Normalize GTIN to 14 digits
const normalizedGTIN = normalizeGTIN(parsed.primaryValue);
// Validate check digit
if (!validateCheckDigit(normalizedGTIN)) {
return { valid: false, error: "INVALID_GTIN_CHECK_DIGIT" };
}
// Validate serial if present
if (parsed.qualifiers.find(q => q.ai === "21")) {
const serial = parsed.qualifiers.find(q => q.ai === "21").value;
if (!SERIAL_REGEX.test(serial)) {
return { valid: false, error: "INVALID_SERIAL" };
}
}
return { valid: true, normalizedGTIN };
}
Reference: digital-link-uri.md Section 4
3.3 Step 3: Detect Requester Context
Input: Request headers, query parameters, JWT token Output: Requester context (role, authentication status, permissions)
Priority Order (highest to lowest):
- JWT role claim - Authenticated role from valid token
- linkType parameter - Specific link type implies required role
- context parameter - Explicit context hint (requires auth for non-consumer)
- Accept header - Content negotiation hint
- Default - Consumer role (public view)
type RequesterRole = "consumer" | "brand" | "regulator" | "service_center";
interface RequesterContext {
role: RequesterRole;
authenticated: boolean;
identity?: string; // Subject DID if authenticated
brandDID?: string; // For brand role
jurisdiction?: string; // For regulator role
serviceTypes?: string[]; // For service_center role
}
async function detectContext(
authHeader: string | null,
linkType: string | null,
contextParam: string | null,
acceptHeader: string | null
): Promise<RequesterContext> {
// Priority 1: JWT authentication
if (authHeader?.startsWith("Bearer ")) {
const token = authHeader.slice(7);
const claims = await verifyJWT(token);
if (claims) {
return {
role: claims.role as RequesterRole,
authenticated: true,
identity: claims.sub,
brandDID: claims.brand_did,
jurisdiction: claims.jurisdiction,
serviceTypes: claims.service_types
};
}
}
// Priority 2-5: See context-routing.md
// ...
// Default: Consumer (public)
return { role: "consumer", authenticated: false };
}
Reference: context-routing.md
3.4 Step 4: Build Galileo DID
Input: Validated identifiers
Output: did:galileo identifier string
Mapping Pattern:
GS1 URI: https://id.galileoprotocol.io/{'ai'}/{'value'}/{'ai2'}/{'value2'}
did:galileo: did:galileo:{'ai'}:{'value'}:{'ai2'}:{'value2'}
Examples:
| GS1 Digital Link URI | did:galileo DID |
|---------------------|-----------------|
| /01/09506000134352/21/ABC123 | did:galileo:01:09506000134352:21:ABC123 |
| /01/09506000134352 | did:galileo:01:09506000134352 |
| /8006/095060001343520102/21/SET001 | did:galileo:8006:095060001343520102:21:SET001 |
function buildGalileoDID(parsed: ParsedGS1URI, normalizedGTIN: string): string {
let did = `did:galileo:${parsed.primaryAI}:${'normalizedGTIN'}`;
for (const qualifier of parsed.qualifiers) {
did += `:${qualifier.ai}:${qualifier.value}`;
}
return did;
}
Reference: DID-METHOD.md Section 2
3.5 Step 5: Query On-Chain Registry
Input: Galileo DID Output: ProductRecord (controller, contentHash, active status)
interface ProductRecord {
didHash: string; // keccak256 of normalized DID
controller: string; // Brand ONCHAINID address
contentHash: string; // SHA-256 of off-chain document
createdAt: number; // Block timestamp
updatedAt: number; // Last modification timestamp
active: boolean; // false = decommissioned
deactivationReason?: string;
}
async function queryRegistry(did: string): Promise<ProductRecord | null> {
// Normalize DID per DID-METHOD.md Section 2.6:
// - Lowercase method portion (did:galileo:)
// - Preserve AI values and serial case (serials are case-sensitive)
const normalizedDID = normalizeDID(did);
const didHash = keccak256(normalizedDID);
const record = await productRegistry.getRecord(didHash);
if (record.createdAt === 0) {
return null; // Not registered
}
return record;
}
DID Normalization:
Per DID-METHOD.md Section 2.6:
function normalizeDID(did: string): string {
// Extract method prefix (case-insensitive)
const match = did.match(/^(did:galileo:)(.*)$/i);
if (!match) throw new Error("Invalid did:galileo format");
const identifier = match[2];
const parts = identifier.split(":");
const entityTypes = new Set([
"brand",
"retailer",
"issuer",
"artisan",
"verifier",
"customer",
"regulator"
]);
// Entity DID: normalize entity type + name to lowercase
const possibleEntityType = parts[0].toLowerCase();
if (entityTypes.has(possibleEntityType) && parts.length >= 2) {
const entityName = parts.slice(1).join(":").toLowerCase();
return `did:galileo:${'possibleEntityType'}:${'entityName'}`;
}
// Product DID: preserve identifier case (serials are case-sensitive per GS1)
return `did:galileo:${'identifier'}`;
}
Error Conditions:
- Record not found: Return 404
notFound - Registry unavailable: Return 503
serviceUnavailable
3.6 Step 6: Fetch Off-Chain DID Document
Input: contentHash from ProductRecord Output: Full DID document with service endpoints
interface DIDDocument {
"@context": string[];
id: string; // did:galileo:...
controller: string; // Brand DID
alsoKnownAs?: string[]; // GS1 Digital Link URI
verificationMethod?: object[];
service: ServiceEndpoint[];
}
interface ServiceEndpoint {
id: string;
type: string; // "GalileoDPP", "GS1DigitalLink", etc.
serviceEndpoint: string; // Target URL
}
async function fetchDIDDocument(contentHash: string): Promise<DIDDocument | null> {
// Fetch from off-chain storage (IPFS, S3, etc.)
const document = await offChainStore.get(contentHash);
if (!document) {
await logIntegrityAlert("content_missing", contentHash);
return null;
}
// Verify content integrity
const computedHash = sha256(canonicalizeJSON(document));
if (computedHash !== contentHash) {
await logIntegrityAlert("hash_mismatch", contentHash, computedHash);
// Still return document, but flag for investigation
}
return document;
}
Reference: hybrid-architecture.md
3.7 Step 7: Select Links Based on Context
Input: DID document, requester context, linkType parameter Output: Filtered linkset
async function selectLinks(
didDoc: DIDDocument,
context: RequesterContext,
linkType: string | null,
preferredLanguages: string[]
): Promise<Link[]> {
// Get all available links from DID document services
const allLinks = buildLinksFromServices(didDoc);
// Filter by role access permissions
const allowedTypes = ROLE_LINK_TYPES[context.role];
let filteredLinks = allLinks.filter(link =>
allowedTypes.includes(link.rel)
);
// If specific linkType requested, filter further
if (linkType && linkType !== "linkset") {
filteredLinks = filteredLinks.filter(link =>
link.rel === expandLinkType(linkType)
);
// If privileged link type without auth, return 401
if (filteredLinks.length === 0 && isPrivilegedLinkType(linkType)) {
throw new AuthenticationRequired(linkType);
}
}
// Apply language preference
filteredLinks = applyLanguagePreference(filteredLinks, preferredLanguages);
return filteredLinks;
}
Reference: context-routing.md for ROLE_LINK_TYPES matrix
3.8 Step 8: Return Response
Input: Selected links, request parameters Output: HTTP response (redirect or linkset)
Decision Logic:
function buildResponse(
links: Link[],
linkType: string | null,
acceptHeader: string | null,
anchor: string,
context: RequesterContext
): Response {
// Return linkset if explicitly requested
if (linkType === "linkset" || acceptHeader === "application/linkset+json") {
return buildLinksetResponse(links, anchor, context);
}
// Single link: return 307 redirect
if (links.length === 1) {
return buildRedirectResponse(links[0], anchor, context);
}
// Multiple links matching: return linkset
if (links.length > 1) {
return buildLinksetResponse(links, anchor, context);
}
// No matching links: redirect to default
const defaultLink = getDefaultLink(anchor);
return buildRedirectResponse(defaultLink, anchor, context);
}
4. HTTP Interface
4.1 Primary Endpoint
Product Resolution (GTIN + Serial):
GET https://id.galileoprotocol.io/01/{'gtin'}/21/{'serial'}
Product Resolution (GTIN only):
GET https://id.galileoprotocol.io/01/{'gtin'}
Component Resolution:
GET https://id.galileoprotocol.io/8010/{'cpid'}/21/{'serial'}
Document Resolution:
GET https://id.galileoprotocol.io/253/{'gdti'}
4.2 Linkset Endpoint
Explicit linkset request:
GET https://id.galileoprotocol.io/01/{'gtin'}/21/{'serial'}?linkType=linkset
4.3 Well-Known Endpoint
Resolver metadata per GS1 standard:
GET https://id.galileoprotocol.io/.well-known/gs1resolver
Response:
{
"name": "Galileo Luxury Standard Resolver",
"resolverRoot": "https://id.galileoprotocol.io",
"supportedLinkTypes": [
"https://gs1.org/voc/pip",
"https://gs1.org/voc/sustainabilityInfo",
"https://gs1.org/voc/instructions",
"https://gs1.org/voc/traceability",
"https://gs1.org/voc/certificationInfo",
"https://vocab.galileoprotocol.io/authenticity",
"https://vocab.galileoprotocol.io/internalDPP",
"https://vocab.galileoprotocol.io/auditTrail",
"https://vocab.galileoprotocol.io/serviceInfo"
],
"supportedContextValues": ["consumer", "brand", "regulator", "service_center"],
"supportsLinkset": true,
"conformsTo": "https://ref.gs1.org/standards/resolver/1.2.0"
}
4.4 Request Headers
| Header | Required | Description |
|--------|----------|-------------|
| Accept | No | Content type preference (default: redirect) |
| Accept-Language | No | Language preference (BCP 47) |
| Authorization | No | Bearer token for authenticated access |
Accept Header Values:
| Value | Behavior |
|-------|----------|
| */* or absent | 307 redirect to default link |
| application/linkset+json | Return full linkset |
| text/html | Redirect to HTML representation |
| application/json | Redirect to JSON representation |
4.5 Query Parameters
| Parameter | Values | Description |
|-----------|--------|-------------|
| linkType | GS1 or Galileo link type, or linkset | Specific resource type |
| context | consumer, brand, regulator, service_center | Context hint |
| lang | BCP 47 language tag | Language preference |
Example Requests:
# Default consumer redirect
curl https://id.galileoprotocol.io/01/09506000134352/21/ABC123
# Request linkset
curl "https://id.galileoprotocol.io/01/09506000134352/21/ABC123?linkType=linkset"
# Request specific link type
curl "https://id.galileoprotocol.io/01/09506000134352/21/ABC123?linkType=gs1:pip"
# Authenticated brand access
curl -H "Authorization: Bearer {'jwt'}" \
"https://id.galileoprotocol.io/01/09506000134352/21/ABC123?linkType=galileo:internalDPP"
# Language preference
curl -H "Accept-Language: fr-FR, en;q=0.8" \
"https://id.galileoprotocol.io/01/09506000134352/21/ABC123?linkType=gs1:instructions"
5. Response Types
5.1 Redirect Response (307 Temporary Redirect)
Default response when a single link matches.
Response:
HTTP/1.1 307 Temporary Redirect
Location: https://resolver.galileoprotocol.io/dpp/09506000134352/ABC123
Link: <https://id.galileoprotocol.io/01/09506000134352/21/ABC123?linkType=linkset>; rel="linkset"
Cache-Control: public, max-age=300
Why 307 (not 302 or 303):
- 307 preserves HTTP method (POST stays POST)
- 302 may change method to GET (historical browser behavior)
- 303 explicitly changes to GET (informational redirect)
5.2 Linkset Response (200 OK)
Returned when linkType=linkset or multiple links match.
Response:
HTTP/1.1 200 OK
Content-Type: application/linkset+json
Cache-Control: public, max-age=300
Body:
{
"@context": {
"@vocab": "http://www.iana.org/assignments/relation/",
"anchor": "@id",
"href": "@id",
"linkset": "@graph",
"gs1": "https://gs1.org/voc/",
"galileo": "https://vocab.galileoprotocol.io/"
},
"linkset": [
{
"anchor": "https://id.galileoprotocol.io/01/09506000134352/21/ABC123",
"itemDescription": "Birkin 25 Togo Gold",
"https://gs1.org/voc/defaultLink": [
{
"href": "https://resolver.galileoprotocol.io/dpp/09506000134352/ABC123",
"title": "Digital Product Passport",
"type": "application/ld+json"
}
],
"https://gs1.org/voc/pip": [
{
"href": "https://resolver.galileoprotocol.io/pip/09506000134352/ABC123",
"hreflang": ["en", "fr", "zh"],
"title": "Product Information"
}
],
"https://gs1.org/voc/sustainabilityInfo": [
{
"href": "https://resolver.galileoprotocol.io/sustainability/09506000134352/ABC123",
"title": "Sustainability Data"
}
],
"https://vocab.galileoprotocol.io/authenticity": [
{
"href": "https://resolver.galileoprotocol.io/verify/09506000134352/ABC123",
"title": "Authenticity Verification"
}
]
}
]
}
Reference: linkset-schema.json
5.3 Error Responses
| Status | Code | When | |--------|------|------| | 400 | Bad Request | Invalid URI syntax, GTIN, or serial | | 401 | Unauthorized | Privileged link type without valid auth | | 403 | Forbidden | Valid auth but insufficient permissions | | 404 | Not Found | Product not registered | | 410 | Gone | Product deactivated (see Section 6) | | 429 | Too Many Requests | Rate limit exceeded | | 503 | Service Unavailable | Registry or storage unavailable |
6. Deactivated Product Handling
6.1 Deactivation vs Deletion
Critical: Products are NEVER deleted. Deactivation marks a product as no longer active while preserving full provenance history.
| State | HTTP Status | DID Resolvable | History Accessible | |-------|-------------|----------------|-------------------| | Active | 200/307 | Yes | Yes | | Deactivated | 410 | Yes | Yes | | Never existed | 404 | No | No |
6.2 Deactivation Response
HTTP Response:
HTTP/1.1 410 Gone
Content-Type: application/json
Body:
{
"error": "deactivated",
"message": "This product has been deactivated and is no longer active",
"deactivationReason": "destroyed",
"deactivatedAt": "2026-01-15T10:30:00Z",
"did": "did:galileo:01:09506000134352:21:ABC123",
"gs1Uri": "https://id.galileoprotocol.io/01/09506000134352/21/ABC123",
"provenanceLink": "https://resolver.galileoprotocol.io/provenance/09506000134352/ABC123"
}
6.3 Deactivation Reasons
| Reason | Description | Provenance Impact |
|--------|-------------|-------------------|
| destroyed | Physical product destroyed | History preserved |
| lost | Product location unknown | History preserved |
| recalled | Manufacturer recall | Recall notice added |
| counterfeit | Identified as counterfeit | Original DID marked |
| merged | Merged into another DID | Redirect to new DID |
| error | Created in error | Marked as invalid |
6.4 Provenance After Deactivation
Deactivated products remain resolvable for provenance verification:
# Returns 410 with deactivation metadata
curl https://id.galileoprotocol.io/01/09506000134352/21/ABC123
# Provenance link still works
curl https://resolver.galileoprotocol.io/provenance/09506000134352/ABC123
7. Caching Strategy
7.1 Cache-Control by Response Type
| Response Type | Authentication | Cache-Control |
|---------------|----------------|---------------|
| Public consumer view | None | public, max-age=300 |
| Authenticated view | JWT | private, no-store |
| Error (4xx) | Any | no-cache, max-age=60 |
| Deactivated (410) | Any | public, max-age=3600 |
| Linkset (public) | None | public, max-age=300 |
| Linkset (authenticated) | JWT | private, no-store |
7.2 TTL by Content Type
| Content | TTL | Rationale | |---------|-----|-----------| | Product Information Page | 5 minutes | May update with promotions | | Sustainability Data | 1 hour | Rarely changes | | Care Instructions | 24 hours | Static content | | Authenticity Status | 5 minutes | May change on verification | | Audit Trail | No cache | Always fresh, authenticated | | Deactivation Status | 1 hour | Permanent state |
7.3 Cache Headers
Public Response:
Cache-Control: public, max-age=300
ETag: "abc123def456"
Vary: Accept, Accept-Language
Authenticated Response:
Cache-Control: private, no-store
Pragma: no-cache
7.4 Cache Invalidation
Event-Driven Invalidation:
The resolver subscribes to on-chain events for real-time cache invalidation:
event ProductUpdated(
string indexed did,
bytes32 previousHash,
bytes32 newHash,
uint256 timestamp
);
event ProductDeactivated(
string indexed did,
DeactivationReason reason,
uint256 timestamp
);
On event receipt:
- Extract DID from event
- Invalidate all cached responses for that DID
- Pre-warm cache with new data (optional)
7.5 Link Header
All responses include linkset reference:
Link: <https://id.galileoprotocol.io/01/{'gtin'}/21/{'serial'}?linkType=linkset>; rel="linkset"
8. Error Handling
8.1 Error Response Schema
{
"error": "errorType",
"errorCode": "SPECIFIC_ERROR_CODE",
"message": "Human-readable description",
"did": "did:galileo:...",
"gs1Uri": "https://id.galileoprotocol.io/...",
"details": {
"field": "value",
"expected": "value",
"received": "value"
}
}
8.2 Error Codes
| Error Type | Error Code | HTTP Status | Description |
|------------|------------|-------------|-------------|
| invalidIdentifier | INVALID_DOMAIN | 400 | Domain is not id.galileoprotocol.io |
| invalidIdentifier | INVALID_SCHEME | 400 | Scheme is not HTTPS |
| invalidIdentifier | MISSING_IDENTIFIER | 400 | No AI/value pair in URI |
| invalidIdentifier | INVALID_PRIMARY_AI | 400 | Unsupported primary AI |
| invalidIdentifier | INVALID_GTIN_FORMAT | 400 | GTIN not 14 digits |
| invalidIdentifier | INVALID_GTIN_CHECK_DIGIT | 400 | Check digit validation failed |
| invalidIdentifier | INVALID_SERIAL | 400 | Serial format invalid |
| unauthorized | MISSING_TOKEN | 401 | Authorization header required |
| unauthorized | INVALID_TOKEN | 401 | JWT validation failed |
| unauthorized | EXPIRED_TOKEN | 401 | JWT expired |
| forbidden | INSUFFICIENT_ROLE | 403 | Role cannot access link type |
| forbidden | WRONG_BRAND | 403 | Brand DID doesn't match product |
| notFound | NOT_REGISTERED | 404 | DID not in registry |
| deactivated | PRODUCT_DEACTIVATED | 410 | Product is deactivated |
| rateLimited | RATE_LIMIT_EXCEEDED | 429 | Too many requests |
| serverError | REGISTRY_UNAVAILABLE | 503 | On-chain registry error |
| serverError | STORAGE_UNAVAILABLE | 503 | Off-chain storage error |
8.3 Error Response Examples
Invalid GTIN:
{
"error": "invalidIdentifier",
"errorCode": "INVALID_GTIN_CHECK_DIGIT",
"message": "GTIN check digit validation failed",
"gs1Uri": "https://id.galileoprotocol.io/01/09506000134353/21/ABC123",
"details": {
"ai": "01",
"value": "09506000134353",
"expectedCheckDigit": 2,
"receivedCheckDigit": 3
}
}
Authentication Required:
{
"error": "unauthorized",
"errorCode": "MISSING_TOKEN",
"message": "Authentication required for link type galileo:internalDPP",
"gs1Uri": "https://id.galileoprotocol.io/01/09506000134352/21/ABC123",
"details": {
"requestedLinkType": "galileo:internalDPP",
"requiredRole": "brand"
}
}
Insufficient Permissions:
{
"error": "forbidden",
"errorCode": "INSUFFICIENT_ROLE",
"message": "Your role 'consumer' cannot access link type 'galileo:auditTrail'",
"gs1Uri": "https://id.galileoprotocol.io/01/09506000134352/21/ABC123",
"details": {
"yourRole": "consumer",
"requiredRole": ["brand", "regulator"],
"requestedLinkType": "galileo:auditTrail"
}
}
8.4 WWW-Authenticate Header
For 401 responses:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="galileo", error="invalid_token", error_description="JWT signature invalid"
9. Integration Points
9.1 On-Chain Token Registry
The resolver queries the Galileo product registry for authoritative metadata:
interface ProductRegistryInterface {
// Get product record by DID hash
getRecord(didHash: bytes32): ProductRecord;
// Check if product is active
isActive(didHash: bytes32): boolean;
// Get controller (brand) address
getController(didHash: bytes32): address;
}
Contract Address: Configured via environment Network: Polygon (mainnet/amoy) Caching: 5 minutes for read operations
9.2 Off-Chain DPP Storage
The resolver fetches full DID documents from off-chain storage:
interface OffChainStoreInterface {
// Get document by content hash
get(contentHash: string): DIDDocument | null;
// Verify document exists
exists(contentHash: string): boolean;
}
Storage Options:
- IPFS (content-addressed)
- Azure Blob Storage
- AWS S3
Integrity: SHA-256 hash verification against on-chain contentHash
9.3 ONCHAINID Consent Verification
For service center authorization:
interface ONChAINIDInterface {
// Check if identity has valid claim
hasValidClaim(
identity: address,
claimTopic: uint256
): boolean;
// Get claim data
getClaim(
identity: address,
claimTopic: uint256
): ClaimData;
}
Claim Topics:
SERVICE_CENTER:0x10830870ec631edcb6878ba73b73764c94401f5fd6d4b09e57afb7b1ac948ff2AUTHENTICATOR:0xda684ab89dbe929e1da9afb6a82d42762bb88db87f85e2041b5a2867ec6a6767
Reference: claim-topics.md
9.4 Authentication Service
JWT issuance and validation:
interface AuthServiceInterface {
// Validate JWT and extract claims
verifyToken(token: string): JWTClaims | null;
// Get JWKS for signature verification
getJWKS(): JWKS;
}
Endpoint: https://auth.galileoprotocol.io/.well-known/jwks.json
Algorithms: RS256, ES256 (asymmetric only)
Reference: access-control.md
Appendix A: Resolution Flow Examples
Example 1: Consumer Scans Product QR Code
1. Consumer scans QR code on handbag
URI: https://id.galileoprotocol.io/01/09506000134352/21/ABC123
2. Resolver parses URI
- AI 01: GTIN = 09506000134352
- AI 21: Serial = ABC123
- No auth token, no context param
3. Context detection: consumer (default)
4. Build DID: did:galileo:01:09506000134352:21:ABC123
5. Query registry: ProductRecord found, active=true
6. Fetch DID document: 12 service endpoints
7. Filter links for consumer role: 4 links visible
8. Response: 307 redirect to gs1:defaultLink
Location: https://resolver.galileoprotocol.io/dpp/09506000134352/ABC123
Example 2: Brand Admin Requests Audit Trail
1. Brand system requests audit trail
URI: https://id.galileoprotocol.io/01/09506000134352/21/ABC123?linkType=galileo:auditTrail
Authorization: Bearer eyJhbGc...
2. Resolver parses URI and token
3. JWT validation succeeds
- role: "brand"
- brand_did: "did:galileo:brand:hermesparis"
4. Verify brand_did matches product controller
5. Context: brand (authenticated)
6. Build DID, query registry, fetch document
7. Filter links: auditTrail link visible for brand role
8. Response: 307 redirect to audit trail
Location: https://resolver.galileoprotocol.io/audit/09506000134352/ABC123
Cache-Control: private, no-store
Example 3: Deactivated Product Resolution
1. Request for deactivated product
URI: https://id.galileoprotocol.io/01/09506000134352/21/DESTROYED001
2. Parse and validate: OK
3. Query registry: ProductRecord found, active=false, reason="destroyed"
4. Response: 410 Gone
{
"error": "deactivated",
"deactivationReason": "destroyed",
"provenanceLink": "https://resolver.galileoprotocol.io/provenance/..."
}
Appendix B: Related Specifications
| Specification | Relationship | |---------------|--------------| | digital-link-uri.md | URI syntax and validation | | linkset-schema.json | Linkset response schema | | context-routing.md | Role-based view selection | | access-control.md | JWT authentication | | DID-METHOD.md | DID resolution | | claim-topics.md | ONCHAINID claims | | hybrid-architecture.md | Data boundary | | GS1-Conformant Resolver 1.2.0 | Base standard |
Galileo Luxury Standard - Resolver Layer Specification: GSPEC-RESOLVER-002 Classification: Public