@ankhorage/doctor 0.6.0 → 0.7.0
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.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/manifestAnalysis.d.ts.map +1 -1
- package/dist/manifestAnalysis.js +2 -0
- package/dist/manifestAnalysis.js.map +1 -1
- package/dist/secretManifestAnalysis.d.ts +3 -0
- package/dist/secretManifestAnalysis.d.ts.map +1 -0
- package/dist/secretManifestAnalysis.js +96 -0
- package/dist/secretManifestAnalysis.js.map +1 -0
- package/package.json +5 -2
- package/src/diagnostics.ts +10 -0
- package/src/manifestAnalysis.ts +3 -0
- package/src/secretManifestAnalysis.ts +241 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9cdfeaf: Validate canonical secret-store selection and OAuth credential references, reject inline secret values, duplicate provider IDs, invalid callback routes, and enabled OAuth providers without a configured secret store.
|
|
8
|
+
|
|
3
9
|
## 0.6.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# DOCTOR
|
|
5
5
|
|
|
6
|
-
         
|
|
7
7
|
|
|
8
8
|
Executable doctor provider and standalone CLI for lightweight Ankhorage repo and package compliance diagnostics.
|
|
9
9
|
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type DoctorDiagnosticCode = 'field-invalid' | 'field-missing' | 'invalid-ankh-metadata' | 'invalid-app-manifest-json' | 'invalid-package-json' | 'missing-dependency' | 'missing-path' | 'missing-script' | 'provider-source-import-failed' | 'provider-source-missing' | 'target-not-directory' | 'target-not-found' | 'unsupported-target';
|
|
2
2
|
export type DoctorDiagnosticSeverity = 'error' | 'warning';
|
|
3
3
|
export type DoctorPolicyProfile = 'app-manifest' | 'integration-monorepo' | 'public-package' | 'unknown';
|
|
4
|
-
export type DoctorRuleId = 'integration.package.ankh.disallowed' | 'integration.package.private.required' | 'integration.package.workspaces.required' | 'manifest.auth.authorization.engine.valid' | 'manifest.auth.authorization.kind.valid' | 'manifest.auth.authorization.valid-shape' | 'manifest.auth.flow.post-sign-in.distinct' | 'manifest.auth.flow.route.required' | 'manifest.auth.flow.route.unique' | 'manifest.auth.flow.route.valid' | 'manifest.auth.flow.unauthorized-route.valid' | 'manifest.auth.flow.valid-shape' | 'manifest.auth.valid-shape' | 'manifest.infra.valid-shape' | 'manifest.json.readable' | 'manifest.json.valid' | 'manifest.root.valid-shape' | 'manifest.settings.auth-flow.removed' | 'package.ankh.capabilities.match-provider' | 'package.ankh.present.valid-shape' | 'package.ankh.provider-path.required' | 'package.ankh.required-for-provider' | 'package.cli.export.required' | 'package.cli.index.required' | 'package.cli.root-file.disallowed' | 'package.dependencies.ankh-workspace-alias.disallowed' | 'package.dependencies.ankhorage4-source.disallowed' | 'package.dependencies.changesets.required' | 'package.dependencies.devtools.required' | 'package.dependencies.paradox.required' | 'package.dependencies.types-bun.required' | 'package.dependencies.types-node.required' | 'package.dependencies.typescript.required' | 'package.imports.ankh-workspace-alias.disallowed' | 'package.imports.ankhorage4-source.disallowed' | 'package.json.bugs.required' | 'package.json.description.required' | 'package.json.exports.required' | 'package.json.files.required' | 'package.json.homepage.required' | 'package.json.keywords.required' | 'package.json.license.required' | 'package.json.name.required' | 'package.json.package-manager.bun' | 'package.json.package-manager.required' | 'package.json.private.public-package-disallowed' | 'package.json.publish-config.public' | 'package.json.publish-config.required' | 'package.json.repository.required' | 'package.json.type.module' | 'package.json.type.required' | 'package.json.version.required' | 'package.scripts.build.required' | 'package.scripts.changeset-status.required' | 'package.scripts.changeset.required' | 'package.scripts.docs.required' | 'package.scripts.format-check.required' | 'package.scripts.format.required' | 'package.scripts.knip.required' | 'package.scripts.lint-fix.required' | 'package.scripts.lint.required' | 'package.scripts.test.required' | 'package.scripts.typecheck.required' | 'package.scripts.version-packages.required' | 'provider.commands.match-capabilities' | 'provider.commands.match-handlers' | 'provider.source.importable' | 'provider.source.required' | 'repo.changelog.required' | 'repo.changeset.required' | 'repo.license.required' | 'repo.readme.required' | 'repo.workflows.required' | 'studio.dependencies.dnd.required' | 'studio.dependencies.runtime.required' | 'studio.imports.dnd.required' | 'studio.imports.runtime.required' | 'target.package-json.required' | 'target.path.directory' | 'target.path.exists' | 'target.profile.supported' | 'target.repo-markers.required';
|
|
4
|
+
export type DoctorRuleId = 'integration.package.ankh.disallowed' | 'integration.package.private.required' | 'integration.package.workspaces.required' | 'manifest.auth.authorization.engine.valid' | 'manifest.auth.authorization.kind.valid' | 'manifest.auth.authorization.valid-shape' | 'manifest.auth.flow.post-sign-in.distinct' | 'manifest.auth.flow.route.required' | 'manifest.auth.flow.route.unique' | 'manifest.auth.flow.route.valid' | 'manifest.auth.flow.unauthorized-route.valid' | 'manifest.auth.flow.valid-shape' | 'manifest.auth.oauth.callback-route.valid' | 'manifest.auth.oauth.provider.credentials-ref.required' | 'manifest.auth.oauth.provider.credentials-ref.valid' | 'manifest.auth.oauth.provider.duplicate' | 'manifest.auth.oauth.provider.inline-secret.disallowed' | 'manifest.auth.oauth.providers.valid' | 'manifest.auth.oauth.valid-shape' | 'manifest.auth.valid-shape' | 'manifest.infra.valid-shape' | 'manifest.json.readable' | 'manifest.json.valid' | 'manifest.root.valid-shape' | 'manifest.secret-store.provider.required' | 'manifest.secret-store.provider.valid' | 'manifest.secret-store.valid-shape' | 'manifest.settings.auth-flow.removed' | 'package.ankh.capabilities.match-provider' | 'package.ankh.present.valid-shape' | 'package.ankh.provider-path.required' | 'package.ankh.required-for-provider' | 'package.cli.export.required' | 'package.cli.index.required' | 'package.cli.root-file.disallowed' | 'package.dependencies.ankh-workspace-alias.disallowed' | 'package.dependencies.ankhorage4-source.disallowed' | 'package.dependencies.changesets.required' | 'package.dependencies.devtools.required' | 'package.dependencies.paradox.required' | 'package.dependencies.types-bun.required' | 'package.dependencies.types-node.required' | 'package.dependencies.typescript.required' | 'package.imports.ankh-workspace-alias.disallowed' | 'package.imports.ankhorage4-source.disallowed' | 'package.json.bugs.required' | 'package.json.description.required' | 'package.json.exports.required' | 'package.json.files.required' | 'package.json.homepage.required' | 'package.json.keywords.required' | 'package.json.license.required' | 'package.json.name.required' | 'package.json.package-manager.bun' | 'package.json.package-manager.required' | 'package.json.private.public-package-disallowed' | 'package.json.publish-config.public' | 'package.json.publish-config.required' | 'package.json.repository.required' | 'package.json.type.module' | 'package.json.type.required' | 'package.json.version.required' | 'package.scripts.build.required' | 'package.scripts.changeset-status.required' | 'package.scripts.changeset.required' | 'package.scripts.docs.required' | 'package.scripts.format-check.required' | 'package.scripts.format.required' | 'package.scripts.knip.required' | 'package.scripts.lint-fix.required' | 'package.scripts.lint.required' | 'package.scripts.test.required' | 'package.scripts.typecheck.required' | 'package.scripts.version-packages.required' | 'provider.commands.match-capabilities' | 'provider.commands.match-handlers' | 'provider.source.importable' | 'provider.source.required' | 'repo.changelog.required' | 'repo.changeset.required' | 'repo.license.required' | 'repo.readme.required' | 'repo.workflows.required' | 'studio.dependencies.dnd.required' | 'studio.dependencies.runtime.required' | 'studio.imports.dnd.required' | 'studio.imports.runtime.required' | 'target.package-json.required' | 'target.path.directory' | 'target.path.exists' | 'target.profile.supported' | 'target.repo-markers.required';
|
|
5
5
|
export interface DoctorDiagnostic {
|
|
6
6
|
readonly code: DoctorDiagnosticCode;
|
|
7
7
|
readonly message: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC5B,eAAe,GACf,eAAe,GACf,uBAAuB,GACvB,2BAA2B,GAC3B,sBAAsB,GACtB,oBAAoB,GACpB,cAAc,GACd,gBAAgB,GAChB,+BAA+B,GAC/B,yBAAyB,GACzB,sBAAsB,GACtB,kBAAkB,GAClB,oBAAoB,CAAC;AAEzB,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,SAAS,CAAC;AAC3D,MAAM,MAAM,mBAAmB,GAC7B,cAAc,GAAG,sBAAsB,GAAG,gBAAgB,GAAG,SAAS,CAAC;AACzE,MAAM,MAAM,YAAY,GACpB,qCAAqC,GACrC,sCAAsC,GACtC,yCAAyC,GACzC,0CAA0C,GAC1C,wCAAwC,GACxC,yCAAyC,GACzC,0CAA0C,GAC1C,mCAAmC,GACnC,iCAAiC,GACjC,gCAAgC,GAChC,6CAA6C,GAC7C,gCAAgC,GAChC,2BAA2B,GAC3B,4BAA4B,GAC5B,wBAAwB,GACxB,qBAAqB,GACrB,2BAA2B,GAC3B,qCAAqC,GACrC,0CAA0C,GAC1C,kCAAkC,GAClC,qCAAqC,GACrC,oCAAoC,GACpC,6BAA6B,GAC7B,4BAA4B,GAC5B,kCAAkC,GAClC,sDAAsD,GACtD,mDAAmD,GACnD,0CAA0C,GAC1C,wCAAwC,GACxC,uCAAuC,GACvC,yCAAyC,GACzC,0CAA0C,GAC1C,0CAA0C,GAC1C,iDAAiD,GACjD,8CAA8C,GAC9C,4BAA4B,GAC5B,mCAAmC,GACnC,+BAA+B,GAC/B,6BAA6B,GAC7B,gCAAgC,GAChC,gCAAgC,GAChC,+BAA+B,GAC/B,4BAA4B,GAC5B,kCAAkC,GAClC,uCAAuC,GACvC,gDAAgD,GAChD,oCAAoC,GACpC,sCAAsC,GACtC,kCAAkC,GAClC,0BAA0B,GAC1B,4BAA4B,GAC5B,+BAA+B,GAC/B,gCAAgC,GAChC,2CAA2C,GAC3C,oCAAoC,GACpC,+BAA+B,GAC/B,uCAAuC,GACvC,iCAAiC,GACjC,+BAA+B,GAC/B,mCAAmC,GACnC,+BAA+B,GAC/B,+BAA+B,GAC/B,oCAAoC,GACpC,2CAA2C,GAC3C,sCAAsC,GACtC,kCAAkC,GAClC,4BAA4B,GAC5B,0BAA0B,GAC1B,yBAAyB,GACzB,yBAAyB,GACzB,uBAAuB,GACvB,sBAAsB,GACtB,yBAAyB,GACzB,kCAAkC,GAClC,sCAAsC,GACtC,6BAA6B,GAC7B,iCAAiC,GACjC,8BAA8B,GAC9B,uBAAuB,GACvB,oBAAoB,GACpB,0BAA0B,GAC1B,8BAA8B,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;CAC7C;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAEtF;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAExF"}
|
|
1
|
+
{"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAC5B,eAAe,GACf,eAAe,GACf,uBAAuB,GACvB,2BAA2B,GAC3B,sBAAsB,GACtB,oBAAoB,GACpB,cAAc,GACd,gBAAgB,GAChB,+BAA+B,GAC/B,yBAAyB,GACzB,sBAAsB,GACtB,kBAAkB,GAClB,oBAAoB,CAAC;AAEzB,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,SAAS,CAAC;AAC3D,MAAM,MAAM,mBAAmB,GAC7B,cAAc,GAAG,sBAAsB,GAAG,gBAAgB,GAAG,SAAS,CAAC;AACzE,MAAM,MAAM,YAAY,GACpB,qCAAqC,GACrC,sCAAsC,GACtC,yCAAyC,GACzC,0CAA0C,GAC1C,wCAAwC,GACxC,yCAAyC,GACzC,0CAA0C,GAC1C,mCAAmC,GACnC,iCAAiC,GACjC,gCAAgC,GAChC,6CAA6C,GAC7C,gCAAgC,GAChC,0CAA0C,GAC1C,uDAAuD,GACvD,oDAAoD,GACpD,wCAAwC,GACxC,uDAAuD,GACvD,qCAAqC,GACrC,iCAAiC,GACjC,2BAA2B,GAC3B,4BAA4B,GAC5B,wBAAwB,GACxB,qBAAqB,GACrB,2BAA2B,GAC3B,yCAAyC,GACzC,sCAAsC,GACtC,mCAAmC,GACnC,qCAAqC,GACrC,0CAA0C,GAC1C,kCAAkC,GAClC,qCAAqC,GACrC,oCAAoC,GACpC,6BAA6B,GAC7B,4BAA4B,GAC5B,kCAAkC,GAClC,sDAAsD,GACtD,mDAAmD,GACnD,0CAA0C,GAC1C,wCAAwC,GACxC,uCAAuC,GACvC,yCAAyC,GACzC,0CAA0C,GAC1C,0CAA0C,GAC1C,iDAAiD,GACjD,8CAA8C,GAC9C,4BAA4B,GAC5B,mCAAmC,GACnC,+BAA+B,GAC/B,6BAA6B,GAC7B,gCAAgC,GAChC,gCAAgC,GAChC,+BAA+B,GAC/B,4BAA4B,GAC5B,kCAAkC,GAClC,uCAAuC,GACvC,gDAAgD,GAChD,oCAAoC,GACpC,sCAAsC,GACtC,kCAAkC,GAClC,0BAA0B,GAC1B,4BAA4B,GAC5B,+BAA+B,GAC/B,gCAAgC,GAChC,2CAA2C,GAC3C,oCAAoC,GACpC,+BAA+B,GAC/B,uCAAuC,GACvC,iCAAiC,GACjC,+BAA+B,GAC/B,mCAAmC,GACnC,+BAA+B,GAC/B,+BAA+B,GAC/B,oCAAoC,GACpC,2CAA2C,GAC3C,sCAAsC,GACtC,kCAAkC,GAClC,4BAA4B,GAC5B,0BAA0B,GAC1B,yBAAyB,GACzB,yBAAyB,GACzB,uBAAuB,GACvB,sBAAsB,GACtB,yBAAyB,GACzB,kCAAkC,GAClC,sCAAsC,GACtC,6BAA6B,GAC7B,iCAAiC,GACjC,8BAA8B,GAC9B,uBAAuB,GACvB,oBAAoB,GACpB,0BAA0B,GAC1B,8BAA8B,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,wBAAwB,CAAC;CAC7C;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAEtF;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,CAExF"}
|
package/dist/diagnostics.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAyHA,MAAM,UAAU,qBAAqB,CAAC,WAAwC;IAC5E,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;AACpF,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,WAAwC;IAC9E,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,MAAM,CAAC;AACtF,CAAC","sourcesContent":["export type DoctorDiagnosticCode =\n | 'field-invalid'\n | 'field-missing'\n | 'invalid-ankh-metadata'\n | 'invalid-app-manifest-json'\n | 'invalid-package-json'\n | 'missing-dependency'\n | 'missing-path'\n | 'missing-script'\n | 'provider-source-import-failed'\n | 'provider-source-missing'\n | 'target-not-directory'\n | 'target-not-found'\n | 'unsupported-target';\n\nexport type DoctorDiagnosticSeverity = 'error' | 'warning';\nexport type DoctorPolicyProfile =\n 'app-manifest' | 'integration-monorepo' | 'public-package' | 'unknown';\nexport type DoctorRuleId =\n | 'integration.package.ankh.disallowed'\n | 'integration.package.private.required'\n | 'integration.package.workspaces.required'\n | 'manifest.auth.authorization.engine.valid'\n | 'manifest.auth.authorization.kind.valid'\n | 'manifest.auth.authorization.valid-shape'\n | 'manifest.auth.flow.post-sign-in.distinct'\n | 'manifest.auth.flow.route.required'\n | 'manifest.auth.flow.route.unique'\n | 'manifest.auth.flow.route.valid'\n | 'manifest.auth.flow.unauthorized-route.valid'\n | 'manifest.auth.flow.valid-shape'\n | 'manifest.auth.oauth.callback-route.valid'\n | 'manifest.auth.oauth.provider.credentials-ref.required'\n | 'manifest.auth.oauth.provider.credentials-ref.valid'\n | 'manifest.auth.oauth.provider.duplicate'\n | 'manifest.auth.oauth.provider.inline-secret.disallowed'\n | 'manifest.auth.oauth.providers.valid'\n | 'manifest.auth.oauth.valid-shape'\n | 'manifest.auth.valid-shape'\n | 'manifest.infra.valid-shape'\n | 'manifest.json.readable'\n | 'manifest.json.valid'\n | 'manifest.root.valid-shape'\n | 'manifest.secret-store.provider.required'\n | 'manifest.secret-store.provider.valid'\n | 'manifest.secret-store.valid-shape'\n | 'manifest.settings.auth-flow.removed'\n | 'package.ankh.capabilities.match-provider'\n | 'package.ankh.present.valid-shape'\n | 'package.ankh.provider-path.required'\n | 'package.ankh.required-for-provider'\n | 'package.cli.export.required'\n | 'package.cli.index.required'\n | 'package.cli.root-file.disallowed'\n | 'package.dependencies.ankh-workspace-alias.disallowed'\n | 'package.dependencies.ankhorage4-source.disallowed'\n | 'package.dependencies.changesets.required'\n | 'package.dependencies.devtools.required'\n | 'package.dependencies.paradox.required'\n | 'package.dependencies.types-bun.required'\n | 'package.dependencies.types-node.required'\n | 'package.dependencies.typescript.required'\n | 'package.imports.ankh-workspace-alias.disallowed'\n | 'package.imports.ankhorage4-source.disallowed'\n | 'package.json.bugs.required'\n | 'package.json.description.required'\n | 'package.json.exports.required'\n | 'package.json.files.required'\n | 'package.json.homepage.required'\n | 'package.json.keywords.required'\n | 'package.json.license.required'\n | 'package.json.name.required'\n | 'package.json.package-manager.bun'\n | 'package.json.package-manager.required'\n | 'package.json.private.public-package-disallowed'\n | 'package.json.publish-config.public'\n | 'package.json.publish-config.required'\n | 'package.json.repository.required'\n | 'package.json.type.module'\n | 'package.json.type.required'\n | 'package.json.version.required'\n | 'package.scripts.build.required'\n | 'package.scripts.changeset-status.required'\n | 'package.scripts.changeset.required'\n | 'package.scripts.docs.required'\n | 'package.scripts.format-check.required'\n | 'package.scripts.format.required'\n | 'package.scripts.knip.required'\n | 'package.scripts.lint-fix.required'\n | 'package.scripts.lint.required'\n | 'package.scripts.test.required'\n | 'package.scripts.typecheck.required'\n | 'package.scripts.version-packages.required'\n | 'provider.commands.match-capabilities'\n | 'provider.commands.match-handlers'\n | 'provider.source.importable'\n | 'provider.source.required'\n | 'repo.changelog.required'\n | 'repo.changeset.required'\n | 'repo.license.required'\n | 'repo.readme.required'\n | 'repo.workflows.required'\n | 'studio.dependencies.dnd.required'\n | 'studio.dependencies.runtime.required'\n | 'studio.imports.dnd.required'\n | 'studio.imports.runtime.required'\n | 'target.package-json.required'\n | 'target.path.directory'\n | 'target.path.exists'\n | 'target.profile.supported'\n | 'target.repo-markers.required';\n\nexport interface DoctorDiagnostic {\n readonly code: DoctorDiagnosticCode;\n readonly message: string;\n readonly path: string;\n readonly profile: DoctorPolicyProfile;\n readonly ruleId: DoctorRuleId;\n readonly severity: DoctorDiagnosticSeverity;\n}\n\nexport function countErrorDiagnostics(diagnostics: readonly DoctorDiagnostic[]): number {\n return diagnostics.filter((diagnostic) => diagnostic.severity === 'error').length;\n}\n\nexport function countWarningDiagnostics(diagnostics: readonly DoctorDiagnostic[]): number {\n return diagnostics.filter((diagnostic) => diagnostic.severity === 'warning').length;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifestAnalysis.d.ts","sourceRoot":"","sources":["../src/manifestAnalysis.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EAGrB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,gBAAgB,EAIjB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"manifestAnalysis.d.ts","sourceRoot":"","sources":["../src/manifestAnalysis.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EAGrB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,gBAAgB,EAIjB,MAAM,kBAAkB,CAAC;AAsB1B,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAmCtC;AAED,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAgC1F;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,OAAO,EACjB,YAAY,SAAkB,GAC7B,gBAAgB,EAAE,CAuEpB"}
|
package/dist/manifestAnalysis.js
CHANGED
|
@@ -2,6 +2,7 @@ import { promises as fs } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { AUTHZ_ENGINES, AUTHZ_KINDS } from '@ankhorage/contracts';
|
|
4
4
|
import { resolveAuthFlow } from '@ankhorage/contracts/auth';
|
|
5
|
+
import { analyzeSecretStoreManifest } from './secretManifestAnalysis.js';
|
|
5
6
|
const MANIFEST_PROFILE = 'app-manifest';
|
|
6
7
|
const AUTH_ACTION_ROUTE_KEYS = [
|
|
7
8
|
'signInRoute',
|
|
@@ -112,6 +113,7 @@ export function analyzeAppManifest(manifest, manifestPath = 'manifest.json') {
|
|
|
112
113
|
}));
|
|
113
114
|
return diagnostics;
|
|
114
115
|
}
|
|
116
|
+
diagnostics.push(...analyzeSecretStoreManifest(infra, manifestPath));
|
|
115
117
|
const { auth } = infra;
|
|
116
118
|
if (auth === undefined) {
|
|
117
119
|
return diagnostics;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifestAnalysis.js","sourceRoot":"","sources":["../src/manifestAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nimport { AUTHZ_ENGINES, AUTHZ_KINDS } from '@ankhorage/contracts';\nimport { type AuthFlowConfig, resolveAuthFlow } from '@ankhorage/contracts/auth';\n\nimport type {\n DoctorAnalysisRequest,\n DoctorAnalysisResult,\n DoctorFixPlan,\n DoctorPlannedChange,\n} from './analysis.js';\nimport type {\n DoctorDiagnostic,\n DoctorDiagnosticCode,\n DoctorPolicyProfile,\n DoctorRuleId,\n} from './diagnostics.js';\n\nconst MANIFEST_PROFILE = 'app-manifest' satisfies DoctorPolicyProfile;\nconst AUTH_ACTION_ROUTE_KEYS = [\n 'signInRoute',\n 'signUpRoute',\n 'signOutRoute',\n 'forgotPasswordRoute',\n 'otpRoute',\n] as const satisfies readonly (keyof AuthFlowConfig)[];\nconst OPTIONAL_ROUTE_KEYS = [\n 'signUpRoute',\n 'signOutRoute',\n 'forgotPasswordRoute',\n 'otpRoute',\n 'unauthorizedRoute',\n] as const satisfies readonly (keyof AuthFlowConfig)[];\n\ntype AuthActionRouteKey = (typeof AUTH_ACTION_ROUTE_KEYS)[number];\ntype OptionalRouteKey = (typeof OPTIONAL_ROUTE_KEYS)[number];\n\nexport async function analyzeAppManifestTarget(\n request: DoctorAnalysisRequest,\n): Promise<DoctorAnalysisResult | null> {\n const targetPath = path.resolve(request.cwd, request.inputPath ?? '.');\n const stats = await statOrNull(targetPath);\n\n if (\n stats === null ||\n !stats.isFile() ||\n path.extname(targetPath).toLowerCase() !== '.json' ||\n (request.mode !== 'validate' && request.mode !== 'fix')\n ) {\n return null;\n }\n\n const diagnostics = await analyzeAppManifestFile(targetPath);\n const plannedChanges: DoctorPlannedChange[] = [];\n const fixPlan: DoctorFixPlan | null =\n request.mode === 'fix'\n ? {\n changes: plannedChanges,\n diagnostics,\n profile: MANIFEST_PROFILE,\n targetPath,\n }\n : null;\n\n return {\n appliedChecks: ['manifest'],\n diagnostics,\n fixPlan,\n hasPackageJson: false,\n plannedChanges,\n profile: MANIFEST_PROFILE,\n repoMarkers: [],\n targetPath,\n };\n}\n\nexport async function analyzeAppManifestFile(filePath: string): Promise<DoctorDiagnostic[]> {\n let source: string;\n\n try {\n source = await fs.readFile(filePath, 'utf8');\n } catch (error) {\n return [\n createManifestDiagnostic({\n code: 'invalid-app-manifest-json',\n message: `Could not read app manifest JSON: ${toErrorMessage(error)}`,\n path: filePath,\n ruleId: 'manifest.json.readable',\n }),\n ];\n }\n\n let manifest: unknown;\n\n try {\n manifest = JSON.parse(source) as unknown;\n } catch (error) {\n return [\n createManifestDiagnostic({\n code: 'invalid-app-manifest-json',\n message: `App manifest must contain valid JSON: ${toErrorMessage(error)}`,\n path: filePath,\n ruleId: 'manifest.json.valid',\n }),\n ];\n }\n\n return analyzeAppManifest(manifest, filePath);\n}\n\nexport function analyzeAppManifest(\n manifest: unknown,\n manifestPath = 'manifest.json',\n): DoctorDiagnostic[] {\n if (!isRecord(manifest)) {\n return [\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'App manifest root must be a JSON object.',\n path: manifestPath,\n ruleId: 'manifest.root.valid-shape',\n }),\n ];\n }\n\n const diagnostics: DoctorDiagnostic[] = [];\n const { settings } = manifest;\n\n if (isRecord(settings) && hasOwn(settings, 'authFlow')) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message:\n 'settings.authFlow was removed. Move this configuration manually to infra.auth.flow; Doctor will not migrate it automatically.',\n path: manifestPath,\n ruleId: 'manifest.settings.auth-flow.removed',\n }),\n );\n }\n\n const { infra } = manifest;\n if (infra === undefined) {\n return diagnostics;\n }\n\n if (!isRecord(infra)) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'manifest.infra must be a JSON object when present.',\n path: manifestPath,\n ruleId: 'manifest.infra.valid-shape',\n }),\n );\n return diagnostics;\n }\n\n const { auth } = infra;\n if (auth === undefined) {\n return diagnostics;\n }\n\n if (!isRecord(auth)) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'manifest.infra.auth must be a JSON object when present.',\n path: manifestPath,\n ruleId: 'manifest.auth.valid-shape',\n }),\n );\n return diagnostics;\n }\n\n const flow = parseAuthFlow(auth.flow, manifestPath, diagnostics);\n if (flow !== null) {\n validateAuthFlowRelationships(flow, manifestPath, diagnostics);\n }\n\n validateAuthorization(auth.authorization, manifestPath, diagnostics);\n\n return diagnostics;\n}\n\nfunction parseAuthFlow(\n value: unknown,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): AuthFlowConfig | null {\n if (value === undefined) {\n return resolveAuthFlow();\n }\n\n if (!isRecord(value)) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'manifest.infra.auth.flow must be a JSON object when present.',\n path: manifestPath,\n ruleId: 'manifest.auth.flow.valid-shape',\n }),\n );\n return null;\n }\n\n const signInRoute = readRequiredRoute(value, 'signInRoute', manifestPath, diagnostics);\n const postSignInRoute = readRequiredRoute(value, 'postSignInRoute', manifestPath, diagnostics);\n const optionalRoutes = readOptionalRoutes(value, manifestPath, diagnostics);\n\n if (signInRoute === null || postSignInRoute === null || optionalRoutes === null) {\n return null;\n }\n\n return resolveAuthFlow({\n signInRoute,\n postSignInRoute,\n ...optionalRoutes,\n });\n}\n\nfunction readRequiredRoute(\n flow: Record<string, unknown>,\n key: 'postSignInRoute' | 'signInRoute',\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): string | null {\n const value = flow[key];\n\n if (typeof value !== 'string' || value.trim() === '') {\n diagnostics.push(\n createManifestDiagnostic({\n code: value === undefined ? 'field-missing' : 'field-invalid',\n message: `manifest.infra.auth.flow.${key} must be a non-empty string.`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.route.required',\n }),\n );\n return null;\n }\n\n return value;\n}\n\nfunction readOptionalRoutes(\n flow: Record<string, unknown>,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): Pick<AuthFlowConfig, OptionalRouteKey> | null {\n const routes: Partial<Pick<AuthFlowConfig, OptionalRouteKey>> = {};\n let valid = true;\n\n for (const key of OPTIONAL_ROUTE_KEYS) {\n const value = flow[key];\n if (value === undefined) {\n continue;\n }\n\n if (typeof value !== 'string' || value.trim() === '') {\n valid = false;\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: `manifest.infra.auth.flow.${key} must be a non-empty string when present.`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.route.valid',\n }),\n );\n continue;\n }\n\n routes[key] = value;\n }\n\n return valid ? routes : null;\n}\n\nfunction validateAuthFlowRelationships(\n flow: AuthFlowConfig,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): void {\n const actionRoutes = AUTH_ACTION_ROUTE_KEYS.flatMap((key) => {\n const value = flow[key];\n return typeof value === 'string' ? [{ key, route: normalizeRoute(value) }] : [];\n });\n const routesByName = new Map<string, AuthActionRouteKey[]>();\n\n for (const entry of actionRoutes) {\n const keys = routesByName.get(entry.route) ?? [];\n keys.push(entry.key);\n routesByName.set(entry.route, keys);\n }\n\n for (const [route, keys] of routesByName) {\n if (keys.length < 2) {\n continue;\n }\n\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: `Auth action routes must be unique; ${keys.join(', ')} all resolve to \"${route}\".`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.route.unique',\n }),\n );\n }\n\n const postSignInRoute = normalizeRoute(flow.postSignInRoute);\n const collidingAction = actionRoutes.find((entry) => entry.route === postSignInRoute);\n if (collidingAction !== undefined) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: `postSignInRoute must resolve outside the auth action routes; it collides with ${collidingAction.key}.`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.post-sign-in.distinct',\n }),\n );\n }\n\n if (flow.unauthorizedRoute === undefined) {\n return;\n }\n\n const unauthorizedRoute = normalizeRoute(flow.unauthorizedRoute);\n const signInRoute = normalizeRoute(flow.signInRoute);\n\n if (unauthorizedRoute === postSignInRoute) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'unauthorizedRoute must not resolve to postSignInRoute.',\n path: manifestPath,\n ruleId: 'manifest.auth.flow.unauthorized-route.valid',\n }),\n );\n }\n\n if (unauthorizedRoute === signInRoute) {\n return;\n }\n\n const collidingActionRoute = actionRoutes.find(\n (entry) => entry.key !== 'signInRoute' && entry.route === unauthorizedRoute,\n );\n if (collidingActionRoute !== undefined) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: `unauthorizedRoute may alias signInRoute, but it must not collide with ${collidingActionRoute.key}.`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.unauthorized-route.valid',\n }),\n );\n }\n}\n\nfunction validateAuthorization(\n value: unknown,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): void {\n if (value === undefined) {\n return;\n }\n\n if (!isRecord(value)) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message:\n 'manifest.infra.auth.authorization must be a JSON object when explicitly configured.',\n path: manifestPath,\n ruleId: 'manifest.auth.authorization.valid-shape',\n }),\n );\n return;\n }\n\n validateAuthorizationField({\n allowed: AUTHZ_KINDS,\n diagnostics,\n fieldName: 'kind',\n manifestPath,\n value: value.kind,\n ruleId: 'manifest.auth.authorization.kind.valid',\n });\n validateAuthorizationField({\n allowed: AUTHZ_ENGINES,\n diagnostics,\n fieldName: 'engine',\n manifestPath,\n value: value.engine,\n ruleId: 'manifest.auth.authorization.engine.valid',\n });\n}\n\nfunction validateAuthorizationField(request: {\n readonly allowed: readonly string[];\n readonly diagnostics: DoctorDiagnostic[];\n readonly fieldName: 'engine' | 'kind';\n readonly manifestPath: string;\n readonly ruleId: DoctorRuleId;\n readonly value: unknown;\n}): void {\n if (typeof request.value === 'string' && request.allowed.includes(request.value)) {\n return;\n }\n\n request.diagnostics.push(\n createManifestDiagnostic({\n code: request.value === undefined ? 'field-missing' : 'field-invalid',\n message: `Explicit authorization.${request.fieldName} must be one of: ${request.allowed.join(', ')}.`,\n path: request.manifestPath,\n ruleId: request.ruleId,\n }),\n );\n}\n\nfunction createManifestDiagnostic(request: {\n readonly code: DoctorDiagnosticCode;\n readonly message: string;\n readonly path: string;\n readonly ruleId: DoctorRuleId;\n}): DoctorDiagnostic {\n return {\n ...request,\n profile: MANIFEST_PROFILE,\n severity: 'error',\n };\n}\n\nfunction normalizeRoute(route: string): string {\n const normalized = route.trim().replace(/^\\/+/, '').replace(/\\/+$/, '');\n return normalized === '' ? 'index' : normalized;\n}\n\nfunction hasOwn(record: Record<string, unknown>, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(record, key);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nasync function statOrNull(filePath: string) {\n try {\n return await fs.stat(filePath);\n } catch {\n return null;\n }\n}\n\nfunction toErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"manifestAnalysis.js","sourceRoot":"","sources":["../src/manifestAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\n\nimport { AUTHZ_ENGINES, AUTHZ_KINDS } from '@ankhorage/contracts';\nimport { type AuthFlowConfig, resolveAuthFlow } from '@ankhorage/contracts/auth';\n\nimport type {\n DoctorAnalysisRequest,\n DoctorAnalysisResult,\n DoctorFixPlan,\n DoctorPlannedChange,\n} from './analysis.js';\nimport type {\n DoctorDiagnostic,\n DoctorDiagnosticCode,\n DoctorPolicyProfile,\n DoctorRuleId,\n} from './diagnostics.js';\nimport { analyzeSecretStoreManifest } from './secretManifestAnalysis.js';\n\nconst MANIFEST_PROFILE = 'app-manifest' satisfies DoctorPolicyProfile;\nconst AUTH_ACTION_ROUTE_KEYS = [\n 'signInRoute',\n 'signUpRoute',\n 'signOutRoute',\n 'forgotPasswordRoute',\n 'otpRoute',\n] as const satisfies readonly (keyof AuthFlowConfig)[];\nconst OPTIONAL_ROUTE_KEYS = [\n 'signUpRoute',\n 'signOutRoute',\n 'forgotPasswordRoute',\n 'otpRoute',\n 'unauthorizedRoute',\n] as const satisfies readonly (keyof AuthFlowConfig)[];\n\ntype AuthActionRouteKey = (typeof AUTH_ACTION_ROUTE_KEYS)[number];\ntype OptionalRouteKey = (typeof OPTIONAL_ROUTE_KEYS)[number];\n\nexport async function analyzeAppManifestTarget(\n request: DoctorAnalysisRequest,\n): Promise<DoctorAnalysisResult | null> {\n const targetPath = path.resolve(request.cwd, request.inputPath ?? '.');\n const stats = await statOrNull(targetPath);\n\n if (\n stats === null ||\n !stats.isFile() ||\n path.extname(targetPath).toLowerCase() !== '.json' ||\n (request.mode !== 'validate' && request.mode !== 'fix')\n ) {\n return null;\n }\n\n const diagnostics = await analyzeAppManifestFile(targetPath);\n const plannedChanges: DoctorPlannedChange[] = [];\n const fixPlan: DoctorFixPlan | null =\n request.mode === 'fix'\n ? {\n changes: plannedChanges,\n diagnostics,\n profile: MANIFEST_PROFILE,\n targetPath,\n }\n : null;\n\n return {\n appliedChecks: ['manifest'],\n diagnostics,\n fixPlan,\n hasPackageJson: false,\n plannedChanges,\n profile: MANIFEST_PROFILE,\n repoMarkers: [],\n targetPath,\n };\n}\n\nexport async function analyzeAppManifestFile(filePath: string): Promise<DoctorDiagnostic[]> {\n let source: string;\n\n try {\n source = await fs.readFile(filePath, 'utf8');\n } catch (error) {\n return [\n createManifestDiagnostic({\n code: 'invalid-app-manifest-json',\n message: `Could not read app manifest JSON: ${toErrorMessage(error)}`,\n path: filePath,\n ruleId: 'manifest.json.readable',\n }),\n ];\n }\n\n let manifest: unknown;\n\n try {\n manifest = JSON.parse(source) as unknown;\n } catch (error) {\n return [\n createManifestDiagnostic({\n code: 'invalid-app-manifest-json',\n message: `App manifest must contain valid JSON: ${toErrorMessage(error)}`,\n path: filePath,\n ruleId: 'manifest.json.valid',\n }),\n ];\n }\n\n return analyzeAppManifest(manifest, filePath);\n}\n\nexport function analyzeAppManifest(\n manifest: unknown,\n manifestPath = 'manifest.json',\n): DoctorDiagnostic[] {\n if (!isRecord(manifest)) {\n return [\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'App manifest root must be a JSON object.',\n path: manifestPath,\n ruleId: 'manifest.root.valid-shape',\n }),\n ];\n }\n\n const diagnostics: DoctorDiagnostic[] = [];\n const { settings } = manifest;\n\n if (isRecord(settings) && hasOwn(settings, 'authFlow')) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message:\n 'settings.authFlow was removed. Move this configuration manually to infra.auth.flow; Doctor will not migrate it automatically.',\n path: manifestPath,\n ruleId: 'manifest.settings.auth-flow.removed',\n }),\n );\n }\n\n const { infra } = manifest;\n if (infra === undefined) {\n return diagnostics;\n }\n\n if (!isRecord(infra)) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'manifest.infra must be a JSON object when present.',\n path: manifestPath,\n ruleId: 'manifest.infra.valid-shape',\n }),\n );\n return diagnostics;\n }\n\n diagnostics.push(...analyzeSecretStoreManifest(infra, manifestPath));\n\n const { auth } = infra;\n if (auth === undefined) {\n return diagnostics;\n }\n\n if (!isRecord(auth)) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'manifest.infra.auth must be a JSON object when present.',\n path: manifestPath,\n ruleId: 'manifest.auth.valid-shape',\n }),\n );\n return diagnostics;\n }\n\n const flow = parseAuthFlow(auth.flow, manifestPath, diagnostics);\n if (flow !== null) {\n validateAuthFlowRelationships(flow, manifestPath, diagnostics);\n }\n\n validateAuthorization(auth.authorization, manifestPath, diagnostics);\n\n return diagnostics;\n}\n\nfunction parseAuthFlow(\n value: unknown,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): AuthFlowConfig | null {\n if (value === undefined) {\n return resolveAuthFlow();\n }\n\n if (!isRecord(value)) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'manifest.infra.auth.flow must be a JSON object when present.',\n path: manifestPath,\n ruleId: 'manifest.auth.flow.valid-shape',\n }),\n );\n return null;\n }\n\n const signInRoute = readRequiredRoute(value, 'signInRoute', manifestPath, diagnostics);\n const postSignInRoute = readRequiredRoute(value, 'postSignInRoute', manifestPath, diagnostics);\n const optionalRoutes = readOptionalRoutes(value, manifestPath, diagnostics);\n\n if (signInRoute === null || postSignInRoute === null || optionalRoutes === null) {\n return null;\n }\n\n return resolveAuthFlow({\n signInRoute,\n postSignInRoute,\n ...optionalRoutes,\n });\n}\n\nfunction readRequiredRoute(\n flow: Record<string, unknown>,\n key: 'postSignInRoute' | 'signInRoute',\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): string | null {\n const value = flow[key];\n\n if (typeof value !== 'string' || value.trim() === '') {\n diagnostics.push(\n createManifestDiagnostic({\n code: value === undefined ? 'field-missing' : 'field-invalid',\n message: `manifest.infra.auth.flow.${key} must be a non-empty string.`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.route.required',\n }),\n );\n return null;\n }\n\n return value;\n}\n\nfunction readOptionalRoutes(\n flow: Record<string, unknown>,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): Pick<AuthFlowConfig, OptionalRouteKey> | null {\n const routes: Partial<Pick<AuthFlowConfig, OptionalRouteKey>> = {};\n let valid = true;\n\n for (const key of OPTIONAL_ROUTE_KEYS) {\n const value = flow[key];\n if (value === undefined) {\n continue;\n }\n\n if (typeof value !== 'string' || value.trim() === '') {\n valid = false;\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: `manifest.infra.auth.flow.${key} must be a non-empty string when present.`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.route.valid',\n }),\n );\n continue;\n }\n\n routes[key] = value;\n }\n\n return valid ? routes : null;\n}\n\nfunction validateAuthFlowRelationships(\n flow: AuthFlowConfig,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): void {\n const actionRoutes = AUTH_ACTION_ROUTE_KEYS.flatMap((key) => {\n const value = flow[key];\n return typeof value === 'string' ? [{ key, route: normalizeRoute(value) }] : [];\n });\n const routesByName = new Map<string, AuthActionRouteKey[]>();\n\n for (const entry of actionRoutes) {\n const keys = routesByName.get(entry.route) ?? [];\n keys.push(entry.key);\n routesByName.set(entry.route, keys);\n }\n\n for (const [route, keys] of routesByName) {\n if (keys.length < 2) {\n continue;\n }\n\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: `Auth action routes must be unique; ${keys.join(', ')} all resolve to \"${route}\".`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.route.unique',\n }),\n );\n }\n\n const postSignInRoute = normalizeRoute(flow.postSignInRoute);\n const collidingAction = actionRoutes.find((entry) => entry.route === postSignInRoute);\n if (collidingAction !== undefined) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: `postSignInRoute must resolve outside the auth action routes; it collides with ${collidingAction.key}.`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.post-sign-in.distinct',\n }),\n );\n }\n\n if (flow.unauthorizedRoute === undefined) {\n return;\n }\n\n const unauthorizedRoute = normalizeRoute(flow.unauthorizedRoute);\n const signInRoute = normalizeRoute(flow.signInRoute);\n\n if (unauthorizedRoute === postSignInRoute) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: 'unauthorizedRoute must not resolve to postSignInRoute.',\n path: manifestPath,\n ruleId: 'manifest.auth.flow.unauthorized-route.valid',\n }),\n );\n }\n\n if (unauthorizedRoute === signInRoute) {\n return;\n }\n\n const collidingActionRoute = actionRoutes.find(\n (entry) => entry.key !== 'signInRoute' && entry.route === unauthorizedRoute,\n );\n if (collidingActionRoute !== undefined) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message: `unauthorizedRoute may alias signInRoute, but it must not collide with ${collidingActionRoute.key}.`,\n path: manifestPath,\n ruleId: 'manifest.auth.flow.unauthorized-route.valid',\n }),\n );\n }\n}\n\nfunction validateAuthorization(\n value: unknown,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): void {\n if (value === undefined) {\n return;\n }\n\n if (!isRecord(value)) {\n diagnostics.push(\n createManifestDiagnostic({\n code: 'field-invalid',\n message:\n 'manifest.infra.auth.authorization must be a JSON object when explicitly configured.',\n path: manifestPath,\n ruleId: 'manifest.auth.authorization.valid-shape',\n }),\n );\n return;\n }\n\n validateAuthorizationField({\n allowed: AUTHZ_KINDS,\n diagnostics,\n fieldName: 'kind',\n manifestPath,\n value: value.kind,\n ruleId: 'manifest.auth.authorization.kind.valid',\n });\n validateAuthorizationField({\n allowed: AUTHZ_ENGINES,\n diagnostics,\n fieldName: 'engine',\n manifestPath,\n value: value.engine,\n ruleId: 'manifest.auth.authorization.engine.valid',\n });\n}\n\nfunction validateAuthorizationField(request: {\n readonly allowed: readonly string[];\n readonly diagnostics: DoctorDiagnostic[];\n readonly fieldName: 'engine' | 'kind';\n readonly manifestPath: string;\n readonly ruleId: DoctorRuleId;\n readonly value: unknown;\n}): void {\n if (typeof request.value === 'string' && request.allowed.includes(request.value)) {\n return;\n }\n\n request.diagnostics.push(\n createManifestDiagnostic({\n code: request.value === undefined ? 'field-missing' : 'field-invalid',\n message: `Explicit authorization.${request.fieldName} must be one of: ${request.allowed.join(', ')}.`,\n path: request.manifestPath,\n ruleId: request.ruleId,\n }),\n );\n}\n\nfunction createManifestDiagnostic(request: {\n readonly code: DoctorDiagnosticCode;\n readonly message: string;\n readonly path: string;\n readonly ruleId: DoctorRuleId;\n}): DoctorDiagnostic {\n return {\n ...request,\n profile: MANIFEST_PROFILE,\n severity: 'error',\n };\n}\n\nfunction normalizeRoute(route: string): string {\n const normalized = route.trim().replace(/^\\/+/, '').replace(/\\/+$/, '');\n return normalized === '' ? 'index' : normalized;\n}\n\nfunction hasOwn(record: Record<string, unknown>, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(record, key);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nasync function statOrNull(filePath: string) {\n try {\n return await fs.stat(filePath);\n } catch {\n return null;\n }\n}\n\nfunction toErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secretManifestAnalysis.d.ts","sourceRoot":"","sources":["../src/secretManifestAnalysis.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAgB,MAAM,kBAAkB,CAAC;AAIvE,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,YAAY,EAAE,MAAM,GACnB,gBAAgB,EAAE,CAiKpB"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { findForbiddenInlineSecretFields, normalizeSecretRef, SECRET_STORE_PROVIDERS, } from '@ankhorage/contracts/secrets';
|
|
2
|
+
const PROFILE = 'app-manifest';
|
|
3
|
+
export function analyzeSecretStoreManifest(infra, manifestPath) {
|
|
4
|
+
const diagnostics = [];
|
|
5
|
+
const secretStoreProvider = validateSecretStore(infra.secretStore, manifestPath, diagnostics);
|
|
6
|
+
if (!isRecord(infra.auth))
|
|
7
|
+
return diagnostics;
|
|
8
|
+
const { oauth } = infra.auth;
|
|
9
|
+
if (oauth === undefined)
|
|
10
|
+
return diagnostics;
|
|
11
|
+
if (!isRecord(oauth)) {
|
|
12
|
+
diagnostics.push(diagnostic('field-invalid', 'manifest.infra.auth.oauth must be a JSON object when present.', manifestPath, 'manifest.auth.oauth.valid-shape'));
|
|
13
|
+
return diagnostics;
|
|
14
|
+
}
|
|
15
|
+
if (typeof oauth.enabled !== 'boolean') {
|
|
16
|
+
diagnostics.push(diagnostic(oauth.enabled === undefined ? 'field-missing' : 'field-invalid', 'manifest.infra.auth.oauth.enabled must be a boolean.', manifestPath, 'manifest.auth.oauth.valid-shape'));
|
|
17
|
+
}
|
|
18
|
+
if (typeof oauth.callbackRoute !== 'string' ||
|
|
19
|
+
oauth.callbackRoute.trim().length === 0 ||
|
|
20
|
+
!oauth.callbackRoute.trim().startsWith('/')) {
|
|
21
|
+
diagnostics.push(diagnostic(oauth.callbackRoute === undefined ? 'field-missing' : 'field-invalid', 'manifest.infra.auth.oauth.callbackRoute must be a non-empty absolute route.', manifestPath, 'manifest.auth.oauth.callback-route.valid'));
|
|
22
|
+
}
|
|
23
|
+
if (!Array.isArray(oauth.providers)) {
|
|
24
|
+
diagnostics.push(diagnostic(oauth.providers === undefined ? 'field-missing' : 'field-invalid', 'manifest.infra.auth.oauth.providers must be an array.', manifestPath, 'manifest.auth.oauth.providers.valid'));
|
|
25
|
+
return diagnostics;
|
|
26
|
+
}
|
|
27
|
+
const enabledProviders = oauth.providers.filter((provider) => isRecord(provider) && provider.enabled !== false);
|
|
28
|
+
if (oauth.enabled === true && enabledProviders.length === 0) {
|
|
29
|
+
diagnostics.push(diagnostic('field-invalid', 'OAuth is enabled, but no OAuth providers are enabled.', manifestPath, 'manifest.auth.oauth.providers.valid'));
|
|
30
|
+
}
|
|
31
|
+
if (enabledProviders.length > 0 && secretStoreProvider === null) {
|
|
32
|
+
diagnostics.push(diagnostic('field-missing', 'Enabled OAuth providers require manifest.infra.secretStore.provider.', manifestPath, 'manifest.secret-store.provider.required'));
|
|
33
|
+
}
|
|
34
|
+
const seenProviderIds = new Set();
|
|
35
|
+
for (const [index, provider] of oauth.providers.entries()) {
|
|
36
|
+
if (!isRecord(provider)) {
|
|
37
|
+
diagnostics.push(diagnostic('field-invalid', `manifest.infra.auth.oauth.providers[${index}] must be a JSON object.`, manifestPath, 'manifest.auth.oauth.providers.valid'));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const providerId = typeof provider.id === 'string' ? provider.id.trim() : '';
|
|
41
|
+
if (providerId.length === 0) {
|
|
42
|
+
diagnostics.push(diagnostic(provider.id === undefined ? 'field-missing' : 'field-invalid', `manifest.infra.auth.oauth.providers[${index}].id must be a non-empty string.`, manifestPath, 'manifest.auth.oauth.providers.valid'));
|
|
43
|
+
}
|
|
44
|
+
else if (seenProviderIds.has(providerId)) {
|
|
45
|
+
diagnostics.push(diagnostic('field-invalid', `OAuth provider id "${providerId}" is configured more than once.`, manifestPath, 'manifest.auth.oauth.provider.duplicate'));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
seenProviderIds.add(providerId);
|
|
49
|
+
}
|
|
50
|
+
for (const field of findForbiddenInlineSecretFields(provider)) {
|
|
51
|
+
diagnostics.push(diagnostic('field-invalid', `Inline OAuth field "${field}" is forbidden. Store the value in the configured secret store and reference it with credentialsRef.`, manifestPath, 'manifest.auth.oauth.provider.inline-secret.disallowed'));
|
|
52
|
+
}
|
|
53
|
+
if (provider.enabled === false)
|
|
54
|
+
continue;
|
|
55
|
+
if (typeof provider.credentialsRef !== 'string') {
|
|
56
|
+
diagnostics.push(diagnostic(provider.credentialsRef === undefined ? 'field-missing' : 'field-invalid', `Enabled OAuth provider "${providerId || index}" requires a credentialsRef.`, manifestPath, 'manifest.auth.oauth.provider.credentials-ref.required'));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const refResult = normalizeSecretRef(provider.credentialsRef);
|
|
60
|
+
if (!refResult.ok || refResult.data !== provider.credentialsRef) {
|
|
61
|
+
diagnostics.push(diagnostic('field-invalid', `OAuth provider "${providerId || index}" credentialsRef must be a canonical logical secret reference.`, manifestPath, 'manifest.auth.oauth.provider.credentials-ref.valid'));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return diagnostics;
|
|
65
|
+
}
|
|
66
|
+
function validateSecretStore(value, manifestPath, diagnostics) {
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
return null;
|
|
69
|
+
if (!isRecord(value)) {
|
|
70
|
+
diagnostics.push(diagnostic('field-invalid', 'manifest.infra.secretStore must be a JSON object when present.', manifestPath, 'manifest.secret-store.valid-shape'));
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (typeof value.provider !== 'string' || value.provider.trim().length === 0) {
|
|
74
|
+
diagnostics.push(diagnostic(value.provider === undefined ? 'field-missing' : 'field-invalid', 'manifest.infra.secretStore.provider must be a non-empty string.', manifestPath, 'manifest.secret-store.provider.required'));
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const provider = value.provider.trim();
|
|
78
|
+
if (!SECRET_STORE_PROVIDERS.includes(provider)) {
|
|
79
|
+
diagnostics.push(diagnostic('field-invalid', `Unsupported secret-store provider "${provider}". Current providers: ${SECRET_STORE_PROVIDERS.join(', ')}.`, manifestPath, 'manifest.secret-store.provider.valid'));
|
|
80
|
+
}
|
|
81
|
+
return provider;
|
|
82
|
+
}
|
|
83
|
+
function diagnostic(code, message, path, ruleId) {
|
|
84
|
+
return {
|
|
85
|
+
code,
|
|
86
|
+
message,
|
|
87
|
+
path,
|
|
88
|
+
profile: PROFILE,
|
|
89
|
+
ruleId,
|
|
90
|
+
severity: 'error',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function isRecord(value) {
|
|
94
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=secretManifestAnalysis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secretManifestAnalysis.js","sourceRoot":"","sources":["../src/secretManifestAnalysis.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n findForbiddenInlineSecretFields,\n normalizeSecretRef,\n SECRET_STORE_PROVIDERS,\n} from '@ankhorage/contracts/secrets';\n\nimport type { DoctorDiagnostic, DoctorRuleId } from './diagnostics.js';\n\nconst PROFILE = 'app-manifest' as const;\n\nexport function analyzeSecretStoreManifest(\n infra: Record<string, unknown>,\n manifestPath: string,\n): DoctorDiagnostic[] {\n const diagnostics: DoctorDiagnostic[] = [];\n const secretStoreProvider = validateSecretStore(infra.secretStore, manifestPath, diagnostics);\n\n if (!isRecord(infra.auth)) return diagnostics;\n const { oauth } = infra.auth;\n if (oauth === undefined) return diagnostics;\n\n if (!isRecord(oauth)) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n 'manifest.infra.auth.oauth must be a JSON object when present.',\n manifestPath,\n 'manifest.auth.oauth.valid-shape',\n ),\n );\n return diagnostics;\n }\n\n if (typeof oauth.enabled !== 'boolean') {\n diagnostics.push(\n diagnostic(\n oauth.enabled === undefined ? 'field-missing' : 'field-invalid',\n 'manifest.infra.auth.oauth.enabled must be a boolean.',\n manifestPath,\n 'manifest.auth.oauth.valid-shape',\n ),\n );\n }\n\n if (\n typeof oauth.callbackRoute !== 'string' ||\n oauth.callbackRoute.trim().length === 0 ||\n !oauth.callbackRoute.trim().startsWith('/')\n ) {\n diagnostics.push(\n diagnostic(\n oauth.callbackRoute === undefined ? 'field-missing' : 'field-invalid',\n 'manifest.infra.auth.oauth.callbackRoute must be a non-empty absolute route.',\n manifestPath,\n 'manifest.auth.oauth.callback-route.valid',\n ),\n );\n }\n\n if (!Array.isArray(oauth.providers)) {\n diagnostics.push(\n diagnostic(\n oauth.providers === undefined ? 'field-missing' : 'field-invalid',\n 'manifest.infra.auth.oauth.providers must be an array.',\n manifestPath,\n 'manifest.auth.oauth.providers.valid',\n ),\n );\n return diagnostics;\n }\n\n const enabledProviders = oauth.providers.filter(\n (provider) => isRecord(provider) && provider.enabled !== false,\n );\n\n if (oauth.enabled === true && enabledProviders.length === 0) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n 'OAuth is enabled, but no OAuth providers are enabled.',\n manifestPath,\n 'manifest.auth.oauth.providers.valid',\n ),\n );\n }\n\n if (enabledProviders.length > 0 && secretStoreProvider === null) {\n diagnostics.push(\n diagnostic(\n 'field-missing',\n 'Enabled OAuth providers require manifest.infra.secretStore.provider.',\n manifestPath,\n 'manifest.secret-store.provider.required',\n ),\n );\n }\n\n const seenProviderIds = new Set<string>();\n\n for (const [index, provider] of oauth.providers.entries()) {\n if (!isRecord(provider)) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n `manifest.infra.auth.oauth.providers[${index}] must be a JSON object.`,\n manifestPath,\n 'manifest.auth.oauth.providers.valid',\n ),\n );\n continue;\n }\n\n const providerId = typeof provider.id === 'string' ? provider.id.trim() : '';\n if (providerId.length === 0) {\n diagnostics.push(\n diagnostic(\n provider.id === undefined ? 'field-missing' : 'field-invalid',\n `manifest.infra.auth.oauth.providers[${index}].id must be a non-empty string.`,\n manifestPath,\n 'manifest.auth.oauth.providers.valid',\n ),\n );\n } else if (seenProviderIds.has(providerId)) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n `OAuth provider id \"${providerId}\" is configured more than once.`,\n manifestPath,\n 'manifest.auth.oauth.provider.duplicate',\n ),\n );\n } else {\n seenProviderIds.add(providerId);\n }\n\n for (const field of findForbiddenInlineSecretFields(provider)) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n `Inline OAuth field \"${field}\" is forbidden. Store the value in the configured secret store and reference it with credentialsRef.`,\n manifestPath,\n 'manifest.auth.oauth.provider.inline-secret.disallowed',\n ),\n );\n }\n\n if (provider.enabled === false) continue;\n\n if (typeof provider.credentialsRef !== 'string') {\n diagnostics.push(\n diagnostic(\n provider.credentialsRef === undefined ? 'field-missing' : 'field-invalid',\n `Enabled OAuth provider \"${providerId || index}\" requires a credentialsRef.`,\n manifestPath,\n 'manifest.auth.oauth.provider.credentials-ref.required',\n ),\n );\n continue;\n }\n\n const refResult = normalizeSecretRef(provider.credentialsRef);\n if (!refResult.ok || refResult.data !== provider.credentialsRef) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n `OAuth provider \"${providerId || index}\" credentialsRef must be a canonical logical secret reference.`,\n manifestPath,\n 'manifest.auth.oauth.provider.credentials-ref.valid',\n ),\n );\n }\n }\n\n return diagnostics;\n}\n\nfunction validateSecretStore(\n value: unknown,\n manifestPath: string,\n diagnostics: DoctorDiagnostic[],\n): string | null {\n if (value === undefined) return null;\n\n if (!isRecord(value)) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n 'manifest.infra.secretStore must be a JSON object when present.',\n manifestPath,\n 'manifest.secret-store.valid-shape',\n ),\n );\n return null;\n }\n\n if (typeof value.provider !== 'string' || value.provider.trim().length === 0) {\n diagnostics.push(\n diagnostic(\n value.provider === undefined ? 'field-missing' : 'field-invalid',\n 'manifest.infra.secretStore.provider must be a non-empty string.',\n manifestPath,\n 'manifest.secret-store.provider.required',\n ),\n );\n return null;\n }\n\n const provider = value.provider.trim();\n if (!(SECRET_STORE_PROVIDERS as readonly string[]).includes(provider)) {\n diagnostics.push(\n diagnostic(\n 'field-invalid',\n `Unsupported secret-store provider \"${provider}\". Current providers: ${SECRET_STORE_PROVIDERS.join(', ')}.`,\n manifestPath,\n 'manifest.secret-store.provider.valid',\n ),\n );\n }\n\n return provider;\n}\n\nfunction diagnostic(\n code: DoctorDiagnostic['code'],\n message: string,\n path: string,\n ruleId: DoctorRuleId,\n): DoctorDiagnostic {\n return {\n code,\n message,\n path,\n profile: PROFILE,\n ruleId,\n severity: 'error',\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Executable Ankh provider and standalone CLI for lightweight repo and package compliance diagnostics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"doctor",
|
|
20
20
|
"cli",
|
|
21
21
|
"compliance",
|
|
22
|
+
"manifest",
|
|
23
|
+
"secret-store",
|
|
24
|
+
"oauth",
|
|
22
25
|
"typescript",
|
|
23
26
|
"bun"
|
|
24
27
|
],
|
|
@@ -74,7 +77,7 @@
|
|
|
74
77
|
"version-packages": "changeset version"
|
|
75
78
|
},
|
|
76
79
|
"dependencies": {
|
|
77
|
-
"@ankhorage/contracts": "^2.
|
|
80
|
+
"@ankhorage/contracts": "^2.1.0"
|
|
78
81
|
},
|
|
79
82
|
"devDependencies": {
|
|
80
83
|
"@ankhorage/ankh": "^0.4.0",
|
package/src/diagnostics.ts
CHANGED
|
@@ -29,11 +29,21 @@ export type DoctorRuleId =
|
|
|
29
29
|
| 'manifest.auth.flow.route.valid'
|
|
30
30
|
| 'manifest.auth.flow.unauthorized-route.valid'
|
|
31
31
|
| 'manifest.auth.flow.valid-shape'
|
|
32
|
+
| 'manifest.auth.oauth.callback-route.valid'
|
|
33
|
+
| 'manifest.auth.oauth.provider.credentials-ref.required'
|
|
34
|
+
| 'manifest.auth.oauth.provider.credentials-ref.valid'
|
|
35
|
+
| 'manifest.auth.oauth.provider.duplicate'
|
|
36
|
+
| 'manifest.auth.oauth.provider.inline-secret.disallowed'
|
|
37
|
+
| 'manifest.auth.oauth.providers.valid'
|
|
38
|
+
| 'manifest.auth.oauth.valid-shape'
|
|
32
39
|
| 'manifest.auth.valid-shape'
|
|
33
40
|
| 'manifest.infra.valid-shape'
|
|
34
41
|
| 'manifest.json.readable'
|
|
35
42
|
| 'manifest.json.valid'
|
|
36
43
|
| 'manifest.root.valid-shape'
|
|
44
|
+
| 'manifest.secret-store.provider.required'
|
|
45
|
+
| 'manifest.secret-store.provider.valid'
|
|
46
|
+
| 'manifest.secret-store.valid-shape'
|
|
37
47
|
| 'manifest.settings.auth-flow.removed'
|
|
38
48
|
| 'package.ankh.capabilities.match-provider'
|
|
39
49
|
| 'package.ankh.present.valid-shape'
|
package/src/manifestAnalysis.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
DoctorPolicyProfile,
|
|
17
17
|
DoctorRuleId,
|
|
18
18
|
} from './diagnostics.js';
|
|
19
|
+
import { analyzeSecretStoreManifest } from './secretManifestAnalysis.js';
|
|
19
20
|
|
|
20
21
|
const MANIFEST_PROFILE = 'app-manifest' satisfies DoctorPolicyProfile;
|
|
21
22
|
const AUTH_ACTION_ROUTE_KEYS = [
|
|
@@ -156,6 +157,8 @@ export function analyzeAppManifest(
|
|
|
156
157
|
return diagnostics;
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
diagnostics.push(...analyzeSecretStoreManifest(infra, manifestPath));
|
|
161
|
+
|
|
159
162
|
const { auth } = infra;
|
|
160
163
|
if (auth === undefined) {
|
|
161
164
|
return diagnostics;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findForbiddenInlineSecretFields,
|
|
3
|
+
normalizeSecretRef,
|
|
4
|
+
SECRET_STORE_PROVIDERS,
|
|
5
|
+
} from '@ankhorage/contracts/secrets';
|
|
6
|
+
|
|
7
|
+
import type { DoctorDiagnostic, DoctorRuleId } from './diagnostics.js';
|
|
8
|
+
|
|
9
|
+
const PROFILE = 'app-manifest' as const;
|
|
10
|
+
|
|
11
|
+
export function analyzeSecretStoreManifest(
|
|
12
|
+
infra: Record<string, unknown>,
|
|
13
|
+
manifestPath: string,
|
|
14
|
+
): DoctorDiagnostic[] {
|
|
15
|
+
const diagnostics: DoctorDiagnostic[] = [];
|
|
16
|
+
const secretStoreProvider = validateSecretStore(infra.secretStore, manifestPath, diagnostics);
|
|
17
|
+
|
|
18
|
+
if (!isRecord(infra.auth)) return diagnostics;
|
|
19
|
+
const { oauth } = infra.auth;
|
|
20
|
+
if (oauth === undefined) return diagnostics;
|
|
21
|
+
|
|
22
|
+
if (!isRecord(oauth)) {
|
|
23
|
+
diagnostics.push(
|
|
24
|
+
diagnostic(
|
|
25
|
+
'field-invalid',
|
|
26
|
+
'manifest.infra.auth.oauth must be a JSON object when present.',
|
|
27
|
+
manifestPath,
|
|
28
|
+
'manifest.auth.oauth.valid-shape',
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
return diagnostics;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof oauth.enabled !== 'boolean') {
|
|
35
|
+
diagnostics.push(
|
|
36
|
+
diagnostic(
|
|
37
|
+
oauth.enabled === undefined ? 'field-missing' : 'field-invalid',
|
|
38
|
+
'manifest.infra.auth.oauth.enabled must be a boolean.',
|
|
39
|
+
manifestPath,
|
|
40
|
+
'manifest.auth.oauth.valid-shape',
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
typeof oauth.callbackRoute !== 'string' ||
|
|
47
|
+
oauth.callbackRoute.trim().length === 0 ||
|
|
48
|
+
!oauth.callbackRoute.trim().startsWith('/')
|
|
49
|
+
) {
|
|
50
|
+
diagnostics.push(
|
|
51
|
+
diagnostic(
|
|
52
|
+
oauth.callbackRoute === undefined ? 'field-missing' : 'field-invalid',
|
|
53
|
+
'manifest.infra.auth.oauth.callbackRoute must be a non-empty absolute route.',
|
|
54
|
+
manifestPath,
|
|
55
|
+
'manifest.auth.oauth.callback-route.valid',
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!Array.isArray(oauth.providers)) {
|
|
61
|
+
diagnostics.push(
|
|
62
|
+
diagnostic(
|
|
63
|
+
oauth.providers === undefined ? 'field-missing' : 'field-invalid',
|
|
64
|
+
'manifest.infra.auth.oauth.providers must be an array.',
|
|
65
|
+
manifestPath,
|
|
66
|
+
'manifest.auth.oauth.providers.valid',
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
return diagnostics;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const enabledProviders = oauth.providers.filter(
|
|
73
|
+
(provider) => isRecord(provider) && provider.enabled !== false,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (oauth.enabled === true && enabledProviders.length === 0) {
|
|
77
|
+
diagnostics.push(
|
|
78
|
+
diagnostic(
|
|
79
|
+
'field-invalid',
|
|
80
|
+
'OAuth is enabled, but no OAuth providers are enabled.',
|
|
81
|
+
manifestPath,
|
|
82
|
+
'manifest.auth.oauth.providers.valid',
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (enabledProviders.length > 0 && secretStoreProvider === null) {
|
|
88
|
+
diagnostics.push(
|
|
89
|
+
diagnostic(
|
|
90
|
+
'field-missing',
|
|
91
|
+
'Enabled OAuth providers require manifest.infra.secretStore.provider.',
|
|
92
|
+
manifestPath,
|
|
93
|
+
'manifest.secret-store.provider.required',
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const seenProviderIds = new Set<string>();
|
|
99
|
+
|
|
100
|
+
for (const [index, provider] of oauth.providers.entries()) {
|
|
101
|
+
if (!isRecord(provider)) {
|
|
102
|
+
diagnostics.push(
|
|
103
|
+
diagnostic(
|
|
104
|
+
'field-invalid',
|
|
105
|
+
`manifest.infra.auth.oauth.providers[${index}] must be a JSON object.`,
|
|
106
|
+
manifestPath,
|
|
107
|
+
'manifest.auth.oauth.providers.valid',
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const providerId = typeof provider.id === 'string' ? provider.id.trim() : '';
|
|
114
|
+
if (providerId.length === 0) {
|
|
115
|
+
diagnostics.push(
|
|
116
|
+
diagnostic(
|
|
117
|
+
provider.id === undefined ? 'field-missing' : 'field-invalid',
|
|
118
|
+
`manifest.infra.auth.oauth.providers[${index}].id must be a non-empty string.`,
|
|
119
|
+
manifestPath,
|
|
120
|
+
'manifest.auth.oauth.providers.valid',
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
} else if (seenProviderIds.has(providerId)) {
|
|
124
|
+
diagnostics.push(
|
|
125
|
+
diagnostic(
|
|
126
|
+
'field-invalid',
|
|
127
|
+
`OAuth provider id "${providerId}" is configured more than once.`,
|
|
128
|
+
manifestPath,
|
|
129
|
+
'manifest.auth.oauth.provider.duplicate',
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
seenProviderIds.add(providerId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const field of findForbiddenInlineSecretFields(provider)) {
|
|
137
|
+
diagnostics.push(
|
|
138
|
+
diagnostic(
|
|
139
|
+
'field-invalid',
|
|
140
|
+
`Inline OAuth field "${field}" is forbidden. Store the value in the configured secret store and reference it with credentialsRef.`,
|
|
141
|
+
manifestPath,
|
|
142
|
+
'manifest.auth.oauth.provider.inline-secret.disallowed',
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (provider.enabled === false) continue;
|
|
148
|
+
|
|
149
|
+
if (typeof provider.credentialsRef !== 'string') {
|
|
150
|
+
diagnostics.push(
|
|
151
|
+
diagnostic(
|
|
152
|
+
provider.credentialsRef === undefined ? 'field-missing' : 'field-invalid',
|
|
153
|
+
`Enabled OAuth provider "${providerId || index}" requires a credentialsRef.`,
|
|
154
|
+
manifestPath,
|
|
155
|
+
'manifest.auth.oauth.provider.credentials-ref.required',
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const refResult = normalizeSecretRef(provider.credentialsRef);
|
|
162
|
+
if (!refResult.ok || refResult.data !== provider.credentialsRef) {
|
|
163
|
+
diagnostics.push(
|
|
164
|
+
diagnostic(
|
|
165
|
+
'field-invalid',
|
|
166
|
+
`OAuth provider "${providerId || index}" credentialsRef must be a canonical logical secret reference.`,
|
|
167
|
+
manifestPath,
|
|
168
|
+
'manifest.auth.oauth.provider.credentials-ref.valid',
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return diagnostics;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function validateSecretStore(
|
|
178
|
+
value: unknown,
|
|
179
|
+
manifestPath: string,
|
|
180
|
+
diagnostics: DoctorDiagnostic[],
|
|
181
|
+
): string | null {
|
|
182
|
+
if (value === undefined) return null;
|
|
183
|
+
|
|
184
|
+
if (!isRecord(value)) {
|
|
185
|
+
diagnostics.push(
|
|
186
|
+
diagnostic(
|
|
187
|
+
'field-invalid',
|
|
188
|
+
'manifest.infra.secretStore must be a JSON object when present.',
|
|
189
|
+
manifestPath,
|
|
190
|
+
'manifest.secret-store.valid-shape',
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof value.provider !== 'string' || value.provider.trim().length === 0) {
|
|
197
|
+
diagnostics.push(
|
|
198
|
+
diagnostic(
|
|
199
|
+
value.provider === undefined ? 'field-missing' : 'field-invalid',
|
|
200
|
+
'manifest.infra.secretStore.provider must be a non-empty string.',
|
|
201
|
+
manifestPath,
|
|
202
|
+
'manifest.secret-store.provider.required',
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const provider = value.provider.trim();
|
|
209
|
+
if (!(SECRET_STORE_PROVIDERS as readonly string[]).includes(provider)) {
|
|
210
|
+
diagnostics.push(
|
|
211
|
+
diagnostic(
|
|
212
|
+
'field-invalid',
|
|
213
|
+
`Unsupported secret-store provider "${provider}". Current providers: ${SECRET_STORE_PROVIDERS.join(', ')}.`,
|
|
214
|
+
manifestPath,
|
|
215
|
+
'manifest.secret-store.provider.valid',
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return provider;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function diagnostic(
|
|
224
|
+
code: DoctorDiagnostic['code'],
|
|
225
|
+
message: string,
|
|
226
|
+
path: string,
|
|
227
|
+
ruleId: DoctorRuleId,
|
|
228
|
+
): DoctorDiagnostic {
|
|
229
|
+
return {
|
|
230
|
+
code,
|
|
231
|
+
message,
|
|
232
|
+
path,
|
|
233
|
+
profile: PROFILE,
|
|
234
|
+
ruleId,
|
|
235
|
+
severity: 'error',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
240
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
241
|
+
}
|